/* This file is part of TALER (C) 2014-2023 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_post-orders-ID-pay.c * @brief handling of POST /orders/$ID/pay requests * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" #include #include #include #include #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" #include "taler-merchant-httpd_private-get-orders.h" /** * How often do we retry the (complex!) database transaction? */ #define MAX_RETRIES 5 /** * Maximum number of coins that we allow per transaction */ #define MAX_COIN_ALLOWED_COINS 1024 /** * How often do we ask the exchange again about our * KYC status? Very rarely, as if the user actively * changes it, we should usually notice anyway. */ #define KYC_RETRY_FREQUENCY GNUNET_TIME_UNIT_WEEKS /** * Information we keep for an individual call to the pay handler. */ struct PayContext; /** * Information kept during a pay request for each coin. */ struct DepositConfirmation { /** * Reference to the main PayContext */ struct PayContext *pc; /** * URL of the exchange that issued this coin. */ char *exchange_url; /** * Details about the coin being deposited. */ struct TALER_EXCHANGE_CoinDepositDetail cdd; /** * Fee charged by the exchange for the deposit operation of this coin. */ struct TALER_Amount deposit_fee; /** * Fee charged by the exchange for the refund operation of this coin. */ struct TALER_Amount refund_fee; /** * Wire fee charged by the exchange of this coin. */ struct TALER_Amount wire_fee; /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the signature of the minimum age (as a single uint8_t), using the * private key to the corresponding age group. Might be all zeroes for no * age attestation. */ struct TALER_AgeAttestation minimum_age_sig; /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the age commitment (i. e. age mask and vector of EdDSA public * keys, one per age group) that went into the mining of the coin. The * SHA256 hash of the mask and the vector of public keys was bound to the * key. */ struct TALER_AgeCommitment age_commitment; /** * Age mask in the denomination that defines the age groups. Only * applicable, if minimum age was required. */ struct TALER_AgeMask age_mask; /** * Offset of this coin into the `dc` array of all coins in the * @e pc. */ unsigned int index; /** * true, if no field "age_commitment" was found in the JSON blob */ bool no_age_commitment; /** * True, if no field "minimum_age_sig" was found in the JSON blob */ bool no_minimum_age_sig; /** * true, if no field "h_age_commitment" was found in the JSON blob */ bool no_h_age_commitment; /** * true if we found this coin in the database. */ bool found_in_db; /** * true if we #deposit_paid_check() matched this coin in the database. */ bool matched_in_db; }; /** * Information kept during a pay request for each exchange. */ struct ExchangeGroup { /** * Payment context this group is part of. */ struct PayContext *pc; /** * Handle to the batch deposit operation we are performing for this * exchange, NULL after the operation is done. */ struct TALER_EXCHANGE_BatchDepositHandle *bdh; /** * Handle for operation to lookup /keys (and auditors) from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_Find2Operation *fo; /** * Handle for operation to lookup /wire from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_WireOperation *gwo; /** * URL of the exchange that issued this coin. Aliases * the exchange URL of one of the coins, do not free! */ const char *exchange_url; /** * true if we already tried a forced /keys download. */ bool tried_force_keys; }; /** * Information we keep for an individual call to the /pay handler. */ struct PayContext { /** * Stored in a DLL. */ struct PayContext *next; /** * Stored in a DLL. */ struct PayContext *prev; /** * Array with @e num_exchange exchanges we are depositing * coins into. */ struct ExchangeGroup **egs; /** * Array with @e coins_cnt coins we are despositing. */ struct DepositConfirmation *dc; /** * MHD connection to return to */ struct MHD_Connection *connection; /** * Details about the client's request. */ struct TMH_HandlerContext *hc; /** * What wire method (of the @e mi) was selected by the wallet? * Set in #parse_pay(). */ struct TMH_WireMethod *wm; /** * Task called when the (suspended) processing for * the /pay request times out. * Happens when we don't get a response from the exchange. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * Response to return, NULL if we don't have one yet. */ struct MHD_Response *response; /** * Our contract (or NULL if not available). */ json_t *contract_terms; /** * Placeholder for #TALER_MHD_parse_post_json() to keep its internal state. */ void *json_parse_context; /** * Optional session id given in @e root. * NULL if not given. */ char *session_id; /** * Transaction ID given in @e root. */ const char *order_id; /** * Fulfillment URL from the contract, or NULL if we don't have one. */ char *fulfillment_url; /** * Serial number of this order in the database (set once we did the lookup). */ uint64_t order_serial; /** * Hashed proposal. */ struct TALER_PrivateContractHashP h_contract_terms; /** * "h_wire" from @e contract_terms. Used to identify * the instance's wire transfer method. */ struct TALER_MerchantWireHashP h_wire; /** * Maximum fee the merchant is willing to pay, from @e root. * Note that IF the total fee of the exchange is higher, that is * acceptable to the merchant if the customer is willing to * pay the difference * (i.e. amount - max_fee <= actual-amount - actual-fee). */ struct TALER_Amount max_fee; /** * Maximum wire fee the merchant is willing to pay, from @e root. * Note that IF the total fee of the exchange is higher, that is * acceptable to the merchant if the customer is willing to * pay the amorized difference. Wire fees are charged over an * aggregate of several translations, hence unlike the deposit * fees, they are amortized over several customer's transactions. * The contract specifies under @e wire_fee_amortization how many * customer's transactions he expects the wire fees to be amortized * over on average. Thus, if the wire fees are larger than * @e max_wire_fee, each customer is expected to contribute * $\frac{actual-wire-fee - max_wire_fee}{wire_fee_amortization}$. * The customer's contribution may be further reduced by the * difference between @e max_fee and the sum of the deposit fees. * * Default is that the merchant is unwilling to pay any wire fees. */ struct TALER_Amount max_wire_fee; /** * Amount from @e root. This is the amount the merchant expects * to make, minus @e max_fee. */ struct TALER_Amount amount; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we were so far paid on * this contract? */ struct TALER_Amount total_paid; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we had to pay in deposit * fees so far on this contract? */ struct TALER_Amount total_fees_paid; /** * Considering all the coins with the "found_in_db" flag * set, what is the total amount we already refunded? */ struct TALER_Amount total_refunded; /** * Wire transfer deadline. How soon would the merchant like the * wire transfer to be executed? */ struct GNUNET_TIME_Timestamp wire_transfer_deadline; /** * Timestamp from @e contract_terms. */ struct GNUNET_TIME_Timestamp timestamp; /** * Refund deadline from @e contract_terms. */ struct GNUNET_TIME_Timestamp refund_deadline; /** * Deadline for the customer to pay for this proposal. */ struct GNUNET_TIME_Timestamp pay_deadline; /** * Set to the POS key, if applicable for this order. */ char *pos_key; /** * Algorithm chosen for generating the confirmation code. */ enum TALER_MerchantConfirmationAlgorithm pos_alg; /** * Number of transactions that the wire fees are expected to be * amortized over. Never zero, defaults (conservateively) to 1. * May be higher if merchants expect many small transactions to * be aggregated and thus wire fees to be reasonably amortized * due to aggregation. */ uint32_t wire_fee_amortization; /** * Minimum age required for this purchase. */ unsigned int minimum_age; /** * Number of coins this payment is made of. Length * of the @e dc array. */ unsigned int coins_cnt; /** * Number of exchanges involved in the payment. Length * of the @e eg array. */ unsigned int num_exchanges; /** * How often have we retried the 'main' transaction? */ unsigned int retry_counter; /** * Number of batch transactions pending. */ unsigned int pending_at_eg; /** * Number of coin deposits pending. */ unsigned int pending; /** * HTTP status code to use for the reply, i.e 200 for "OK". * Special value UINT_MAX is used to indicate hard errors * (no reply, return #MHD_NO). */ unsigned int response_code; /** * #GNUNET_NO if the @e connection was not suspended, * #GNUNET_YES if the @e connection was suspended, * #GNUNET_SYSERR if @e connection was resumed to as * part of #MH_force_pc_resume during shutdown. */ enum GNUNET_GenericReturnValue suspended; }; /** * Active KYC operation with an exchange. */ struct KycContext { /** * Kept in a DLL. */ struct KycContext *next; /** * Kept in a DLL. */ struct KycContext *prev; /** * Looking for the exchange. */ struct TMH_EXCHANGES_Find2Operation *fo; /** * Exchange this is about. */ char *exchange_url; /** * Merchant instance this is for. */ struct TMH_MerchantInstance *mi; /** * Wire method we are checking the status of. */ struct TMH_WireMethod *wm; /** * Handle for the GET /deposits operation. */ struct TALER_EXCHANGE_DepositGetHandle *dg; /** * Contract we are looking up. */ struct TALER_PrivateContractHashP h_contract_terms; /** * Coin we are looking up. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Initial DB timestamp. */ struct GNUNET_TIME_Timestamp kyc_timestamp; /** * Initial KYC status. */ bool kyc_ok; }; /** * Head of active pay context DLL. */ static struct PayContext *pc_head; /** * Tail of active pay context DLL. */ static struct PayContext *pc_tail; /** * Head of active KYC context DLL. */ static struct KycContext *kc_head; /** * Tail of active KYC context DLL. */ static struct KycContext *kc_tail; /** * Free resources used by @a kc. * * @param[in] kc object to free */ static void destroy_kc (struct KycContext *kc) { if (NULL != kc->fo) { TMH_EXCHANGES_keys4exchange_cancel (kc->fo); kc->fo = NULL; } if (NULL != kc->dg) { TALER_EXCHANGE_deposits_get_cancel (kc->dg); kc->dg = NULL; } TMH_instance_decref (kc->mi); kc->mi = NULL; GNUNET_free (kc->exchange_url); GNUNET_CONTAINER_DLL_remove (kc_head, kc_tail, kc); GNUNET_free (kc); } /** * Compute the timeout for a /pay request based on the number of coins * involved. * * @param num_coins number of coins * @returns timeout for the /pay request */ static struct GNUNET_TIME_Relative get_pay_timeout (unsigned int num_coins) { struct GNUNET_TIME_Relative t; /* FIXME: Do some benchmarking to come up with a better timeout. * We've increased this value so the wallet integration test passes again * on my (Florian) machine. */ t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 15 * (1 + (num_coins / 5))); return t; } void TMH_force_pc_resume () { struct KycContext *kc; while (NULL != (kc = kc_head)) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Aborting KYC check at %s\n", kc->exchange_url); destroy_kc (kc); } for (struct PayContext *pc = pc_head; NULL != pc; pc = pc->next) { if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } if (GNUNET_YES == pc->suspended) { pc->suspended = GNUNET_SYSERR; MHD_resume_connection (pc->connection); } } } /** * Resume the given pay context and send the given response. * Stores the response in the @a pc and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param pc payment context * @param response_code response code to use * @param response response data to send back */ static void resume_pay_with_response (struct PayContext *pc, unsigned int response_code, struct MHD_Response *response) { pc->response_code = response_code; pc->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay handling. HTTP status for our reply is %u.\n", response_code); if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } GNUNET_assert (GNUNET_YES == pc->suspended); pc->suspended = GNUNET_NO; MHD_resume_connection (pc->connection); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } /** * Resume payment processing with an error. * * @param pc operation to resume * @param http_status http status code to return * @param ec taler error code to return * @param msg human readable error message */ static void resume_pay_with_error (struct PayContext *pc, unsigned int http_status, enum TALER_ErrorCode ec, const char *msg) { resume_pay_with_response (pc, http_status, TALER_MHD_make_error (ec, msg)); } /** * Custom cleanup routine for a `struct PayContext`. * * @param cls the `struct PayContext` to clean up. */ static void pay_context_cleanup (void *cls) { struct PayContext *pc = cls; if (NULL != pc->timeout_task) { GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } if (NULL != pc->contract_terms) { json_decref (pc->contract_terms); pc->contract_terms = NULL; } for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; TALER_denom_sig_free (&dc->cdd.denom_sig); GNUNET_free (dc->exchange_url); } GNUNET_free (pc->dc); for (unsigned int i = 0; inum_exchanges; i++) { struct ExchangeGroup *eg = pc->egs[i]; if (NULL != eg->fo) TMH_EXCHANGES_keys4exchange_cancel (eg->fo); if (NULL != eg->gwo) TMH_EXCHANGES_wire4exchange_cancel (eg->gwo); GNUNET_free (eg); } GNUNET_free (pc->egs); if (NULL != pc->response) { MHD_destroy_response (pc->response); pc->response = NULL; } GNUNET_free (pc->fulfillment_url); GNUNET_free (pc->session_id); GNUNET_CONTAINER_DLL_remove (pc_head, pc_tail, pc); GNUNET_free (pc->pos_key); GNUNET_free (pc); } /** * Execute the DB transaction. If required (from * soft/serialization errors), the transaction can be * restarted here. * * @param pc payment context to transact */ static void execute_pay_transaction (struct PayContext *pc); /** * Function called with detailed wire transfer data. * * @param cls a `struct KycContext *` * @param dr HTTP response data */ static void deposit_get_callback ( void *cls, const struct TALER_EXCHANGE_GetDepositResponse *dr) { struct KycContext *kc = cls; enum GNUNET_DB_QueryStatus qs; struct GNUNET_TIME_Timestamp now; kc->dg = NULL; now = GNUNET_TIME_timestamp_get (); switch (dr->hr.http_status) { case MHD_HTTP_OK: qs = TMH_db->account_kyc_set_status ( TMH_db->cls, kc->mi->settings.id, &kc->wm->h_wire, kc->exchange_url, 0LL, NULL, /* no signature */ NULL, /* no signature */ now, true, TALER_AML_NORMAL); GNUNET_break (qs > 0); break; case MHD_HTTP_ACCEPTED: qs = TMH_db->account_kyc_set_status ( TMH_db->cls, kc->mi->settings.id, &kc->wm->h_wire, kc->exchange_url, dr->details.accepted.requirement_row, NULL, /* no signature */ NULL, /* no signature */ now, dr->details.accepted.kyc_ok, dr->details.accepted.aml_decision); GNUNET_break (qs > 0); break; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "KYC check failed at %s with unexpected status %u\n", kc->exchange_url, dr->hr.http_status); } destroy_kc (kc); } /** * Function called with the result of our exchange lookup. * * @param cls the `struct KycContext` * @param keys NULL if exchange was not found to be acceptable */ static void process_kyc_with_exchange ( void *cls, struct TALER_EXCHANGE_Keys *keys) { struct KycContext *kc = cls; kc->fo = NULL; if (NULL == keys) { destroy_kc (kc); return; } kc->dg = TALER_EXCHANGE_deposits_get ( merchant_curl_ctx, kc->exchange_url, keys, &kc->mi->merchant_priv, &kc->wm->h_wire, &kc->h_contract_terms, &kc->coin_pub, GNUNET_TIME_UNIT_ZERO, &deposit_get_callback, kc); if (NULL == kc->dg) { GNUNET_break (0); destroy_kc (kc); } } /** * Function called from ``account_kyc_get_status`` * with KYC status information for this merchant. * * @param cls a `struct KycContext *` * @param h_wire hash of the wire account * @param exchange_kyc_serial serial number for the KYC process at the exchange, 0 if unknown * @param payto_uri payto:// URI of the merchant's bank account * @param exchange_url base URL of the exchange for which this is a status * @param last_check when did we last get an update on our KYC status from the exchange * @param kyc_ok true if we satisfied the KYC requirements * @param aml_decision latest AML decision by the exchange */ static void kyc_cb ( void *cls, const struct TALER_MerchantWireHashP *h_wire, uint64_t exchange_kyc_serial, const char *payto_uri, const char *exchange_url, struct GNUNET_TIME_Timestamp last_check, bool kyc_ok, enum TALER_AmlDecisionState aml_decision) { struct KycContext *kc = cls; (void) h_wire; (void) exchange_kyc_serial; (void) payto_uri; (void) exchange_url; kc->kyc_timestamp = last_check; kc->kyc_ok = kyc_ok; /* FIXME: act on aml_decision? */ } /** * Check for our KYC status at @a exchange_url for the * payment of @a pc. First checks if we already have a * positive result from the exchange, and if not checks * with the exchange. * * @param pc payment context to use as starting point * @param eg exchange group of the exchange we are triggering on */ static void check_kyc (struct PayContext *pc, const struct ExchangeGroup *eg) { enum GNUNET_DB_QueryStatus qs; struct KycContext *kc; kc = GNUNET_new (struct KycContext); qs = TMH_db->account_kyc_get_status (TMH_db->cls, pc->hc->instance->settings.id, &pc->wm->h_wire, eg->exchange_url, &kyc_cb, kc); if (qs < 0) { GNUNET_break (0); GNUNET_free (kc); return; } if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) { if (kc->kyc_ok) { GNUNET_free (kc); return; /* we are done */ } if (GNUNET_TIME_relative_cmp ( GNUNET_TIME_absolute_get_duration ( kc->kyc_timestamp.abs_time), <, KYC_RETRY_FREQUENCY)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Not re-checking KYC status at `%s', as we already recently asked\n", eg->exchange_url); GNUNET_free (kc); return; } } kc->mi = pc->hc->instance; kc->mi->rc++; kc->wm = pc->wm; kc->exchange_url = GNUNET_strdup (eg->exchange_url); kc->h_contract_terms = pc->h_contract_terms; /* find one of the coins of the batch */ for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; kc->coin_pub = dc->cdd.coin_pub; break; } GNUNET_CONTAINER_DLL_insert (kc_head, kc_tail, kc); kc->fo = TMH_EXCHANGES_keys4exchange (kc->exchange_url, &process_kyc_with_exchange, kc); if (NULL == kc->fo) { GNUNET_break (0); destroy_kc (kc); } } /** * Handle case where the batch deposit completed * with a status of #MHD_HTTP_OK. * * @param eg group that completed * @param dr response from the server */ static void handle_batch_deposit_ok (struct ExchangeGroup *eg, const struct TALER_EXCHANGE_BatchDepositResult *dr) { struct PayContext *pc = eg->pc; enum GNUNET_DB_QueryStatus qs = GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; /* store result to DB */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Storing successful payment %s (%s) at instance `%s'\n", pc->hc->infix, GNUNET_h2s (&pc->h_contract_terms.hash), pc->hc->instance->settings.id); for (unsigned int r = 0; rpreflight (TMH_db->cls); if (GNUNET_OK != TMH_db->start (TMH_db->cls, "batch-deposit-insert-confirmation")) { resume_pay_with_response ( pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_GENERIC_DB_START_FAILED), TMH_pack_exchange_reply (&dr->hr))); return; } for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; if (dc->found_in_db) continue; /* NOTE: We might want to check if the order was fully paid concurrently by some other wallet here, and if so, issue an auto-refund. Right now, it is possible to over-pay if two wallets literally make a concurrent payment, as the earlier check for 'paid' is not in the same transaction scope as this 'insert' operation. */ GNUNET_assert (j < dr->details.ok.num_signatures); qs = TMH_db->insert_deposit ( TMH_db->cls, pc->hc->instance->settings.id, dr->details.ok.deposit_timestamp, &pc->h_contract_terms, &dc->cdd.coin_pub, dc->exchange_url, &dc->cdd.amount, &dc->deposit_fee, &dc->refund_fee, &dc->wire_fee, &pc->wm->h_wire, &dr->details.ok.exchange_sigs[j++], dr->details.ok.exchange_pub); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_db->rollback (TMH_db->cls); break; } if (0 > qs) { /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); /* Forward error including 'proof' for the body */ resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "insert_deposit"); return; } } qs = TMH_db->commit (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { TMH_db->rollback (TMH_db->cls); continue; } if (GNUNET_DB_STATUS_HARD_ERROR == qs) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, "insert_deposit"); } break; /* DB transaction succeeded */ } /* FOR DB retries */ if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, "insert_deposit"); return; } /* Transaction is done, mark affected coins as complete as well. */ for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; if (dc->found_in_db) continue; dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */ pc->pending--; } check_kyc (pc, eg); } /** * Callback to handle a batch deposit permission's response. * * @param cls a `struct ExchangeGroup` * @param dr HTTP response code details */ static void batch_deposit_cb ( void *cls, const struct TALER_EXCHANGE_BatchDepositResult *dr) { struct ExchangeGroup *eg = cls; struct PayContext *pc = eg->pc; eg->bdh = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Batch deposit completed with status %u\n", dr->hr.http_status); GNUNET_assert (GNUNET_YES == pc->suspended); pc->pending_at_eg--; switch (dr->hr.http_status) { case MHD_HTTP_OK: handle_batch_deposit_ok (eg, dr); if (0 == pc->pending_at_eg) execute_pay_transaction (eg->pc); return; default: GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Deposit operation failed with HTTP code %u/%d\n", dr->hr.http_status, (int) dr->hr.ec); /* Transaction failed */ if (5 == dr->hr.http_status / 100) { /* internal server error at exchange */ resume_pay_with_response (pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), TMH_pack_exchange_reply (&dr->hr))); return; } if (NULL == dr->hr.reply) { /* We can't do anything meaningful here, the exchange did something wrong */ resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED), TMH_pack_exchange_reply (&dr->hr))); return; } /* Forward error, adding the "exchange_url" for which the error was being generated */ if (TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS == dr->hr.ec) { resume_pay_with_response ( pc, MHD_HTTP_CONFLICT, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS), TMH_pack_exchange_reply (&dr->hr), GNUNET_JSON_pack_data_auto ("exchange_url", &eg->exchange_url))); return; } resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS), TMH_pack_exchange_reply (&dr->hr), GNUNET_JSON_pack_data_auto ("exchange_url", &eg->exchange_url))); return; } /* end switch */ } /** * Function called with the result of our exchange keys lookup. * * @param cls the `struct ExchangeGroup` * @param keys the keys of the exchange */ static void process_pay_with_keys ( void *cls, struct TALER_EXCHANGE_Keys *keys) { struct ExchangeGroup *eg = cls; struct PayContext *pc = eg->pc; struct TMH_HandlerContext *hc = pc->hc; unsigned int group_size; eg->fo = NULL; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Processing payment with exchange %s\n", eg->exchange_url); GNUNET_assert (GNUNET_YES == pc->suspended); if (NULL == keys) { GNUNET_break_op (0); pc->pending_at_eg--; resume_pay_with_response ( pc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT))); return; } /* Initiate /batch-deposit operation for all coins of the current exchange (!) */ group_size = 0; for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; const struct TALER_EXCHANGE_DenomPublicKey *denom_details; bool is_age_restricted_denom = false; if (0 != strcmp (eg->exchange_url, pc->dc[i].exchange_url)) continue; if (dc->found_in_db) continue; denom_details = TALER_EXCHANGE_get_denomination_key_by_hash (keys, &dc->cdd.h_denom_pub); if (NULL == denom_details) { pc->pending_at_eg--; resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND), GNUNET_JSON_pack_data_auto ("h_denom_pub", &dc->cdd.h_denom_pub), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_steal ( "exchange_keys", TALER_EXCHANGE_keys_to_json (keys))))); return; } dc->deposit_fee = denom_details->fees.deposit; dc->refund_fee = denom_details->fees.refund; if (GNUNET_TIME_absolute_is_past ( denom_details->expire_deposit.abs_time)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Denomination key offered by client has expired for deposits\n"); pc->pending_at_eg--; resume_pay_with_response ( pc, MHD_HTTP_GONE, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } /* Now that we have the details about the denomination, we can verify age * restriction requirements, if applicable. Note that denominations with an * age_mask equal to zero always pass the age verification. */ is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); if (is_age_restricted_denom && (0 < pc->minimum_age)) { /* Minimum age given and restricted coin provided: We need to verify the * minimum age */ unsigned int code = 0; if (dc->no_age_commitment) { GNUNET_break_op (0); code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING; goto AGE_FAIL; } dc->age_commitment.mask = denom_details->key.age_mask; if (((int) (dc->age_commitment.num + 1)) != __builtin_popcount (dc->age_commitment.mask.bits)) { GNUNET_break_op (0); code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH; goto AGE_FAIL; } if (GNUNET_OK != TALER_age_commitment_verify ( &dc->age_commitment, pc->minimum_age, &dc->minimum_age_sig)) code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; AGE_FAIL: if (0 < code) { pc->pending_at_eg--; GNUNET_free (dc->age_commitment.keys); resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (code), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } /* Age restriction successfully verified! * Calculate the hash of the age commitment. */ TALER_age_commitment_hash (&dc->age_commitment, &dc->cdd.h_age_commitment); GNUNET_free (dc->age_commitment.keys); } else if (is_age_restricted_denom && dc->no_h_age_commitment) { /* The contract did not ask for a minimum_age but the client paid * with a coin that has age restriction enabled. We lack the hash * of the age commitment in this case in order to verify the coin * and to deposit it with the exchange. */ pc->pending_at_eg--; GNUNET_break_op (0); resume_pay_with_response ( pc, MHD_HTTP_BAD_REQUEST, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING), GNUNET_JSON_pack_data_auto ("h_denom_pub", &denom_details->h_key))); return; } group_size++; } if (0 == group_size) { GNUNET_break (0); pc->pending_at_eg--; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Group size zero, %u batch transactions remain pending\n", pc->pending_at_eg); if (0 == pc->pending_at_eg) execute_pay_transaction (pc); return; } { struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; struct TALER_EXCHANGE_DepositContractDetail dcd = { .wire_deadline = pc->wire_transfer_deadline, .merchant_payto_uri = pc->wm->payto_uri, .wire_salt = pc->wm->wire_salt, .h_contract_terms = pc->h_contract_terms, .policy_details = NULL, /* FIXME-oec #7270 */ .timestamp = pc->timestamp, .merchant_pub = hc->instance->merchant_pub, .refund_deadline = pc->refund_deadline }; enum TALER_ErrorCode ec; for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; enum GNUNET_GenericReturnValue ret; if (dc->found_in_db) continue; if (0 != strcmp (dc->exchange_url, eg->exchange_url)) continue; cdds[i] = dc->cdd; ret = TMH_EXCHANGES_lookup_wire_fee (dc->exchange_url, pc->wm->wire_method, &dc->wire_fee); if (GNUNET_OK != ret) { enum TALER_ErrorCode ec; fprintf (stderr, "%d\n", ret); ec = (GNUNET_NO == ret) ? TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED : TALER_EC_MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED; pc->pending_at_eg--; GNUNET_break_op (0); resume_pay_with_response ( pc, TALER_ErrorCode_get_http_status_safe (ec), TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (ec), GNUNET_JSON_pack_string ("wire_method", pc->wm->wire_method))); return; } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Initiating batch deposit with %u coins\n", group_size); eg->bdh = TALER_EXCHANGE_batch_deposit ( merchant_curl_ctx, eg->exchange_url, keys, &dcd, group_size, cdds, &batch_deposit_cb, eg, &ec); if (NULL == eg->bdh) { /* Signature was invalid or some other constraint was not satisfied. If the exchange was unavailable, we'd get that information in the callback. */ pc->pending_at_eg--; GNUNET_break_op (0); resume_pay_with_response ( pc, TALER_ErrorCode_get_http_status_safe (ec), TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec (ec), GNUNET_JSON_pack_string ("exchange_url", eg->exchange_url))); return; } if (TMH_force_audit) TALER_EXCHANGE_batch_deposit_force_dc (eg->bdh); } } /** * Function called with the result of our exchange lookup. * * @param cls the `struct ExchangeGroup` * @param keys the keys of the exchange */ static void process_pay_with_wire ( void *cls, struct TMH_Exchange *wire) { struct ExchangeGroup *eg = cls; struct PayContext *pc = eg->pc; eg->gwo = NULL; if (NULL == wire) { GNUNET_break_op (0); pc->pending_at_eg--; /* FIXME: define more specific error code... */ resume_pay_with_response ( pc, MHD_HTTP_BAD_GATEWAY, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE))); return; } if (GNUNET_OK != TMH_exchange_check_debit (wire, pc->wm)) { GNUNET_break_op (0); pc->pending_at_eg--; resume_pay_with_response ( pc, MHD_HTTP_CONFLICT, TALER_MHD_MAKE_JSON_PACK ( TALER_JSON_pack_ec ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED))); return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Fetching /keys for %s\n", eg->exchange_url); eg->fo = TMH_EXCHANGES_keys4exchange (eg->exchange_url, &process_pay_with_keys, eg); if (NULL == eg->fo) { GNUNET_break (0); pc->pending_at_eg--; resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, "Failed to lookup exchange by URL"); return; } } /** * Start batch deposits for all exchanges involved * in this payment. * * @param pc payment context we are processing */ static void start_batch_deposits (struct PayContext *pc) { for (unsigned int i = 0; inum_exchanges; i++) { struct ExchangeGroup *eg = pc->egs[i]; bool have_coins = false; for (unsigned int j = 0; jcoins_cnt; j++) { struct DepositConfirmation *dc = &pc->dc[j]; if (0 != strcmp (eg->exchange_url, pc->dc[j].exchange_url)) continue; if (dc->found_in_db) continue; have_coins = true; break; } if (! have_coins) continue; /* no coins left to deposit at this exchange */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Getting /wire details for %s\n", eg->exchange_url); eg->gwo = TMH_EXCHANGES_wire4exchange (eg->exchange_url, &process_pay_with_wire, eg); if (NULL == eg->gwo) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED, "Failed to lookup exchange by URL"); return; } pc->pending_at_eg++; } if (0 == pc->pending_at_eg) execute_pay_transaction (pc); } /** * Function called with information about a coin that was deposited. * * @param cls closure * @param exchange_url exchange where @a coin_pub was deposited * @param coin_pub public key of the coin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin * @param wire_fee wire fee the exchange of this coin charges */ static void check_coin_paid (void *cls, const char *exchange_url, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_Amount *refund_fee, const struct TALER_Amount *wire_fee) { struct PayContext *pc = cls; for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; if (dc->found_in_db) continue; /* processed earlier, skip "expensive" memcmp() */ /* Get matching coin from results*/ if ( (0 != GNUNET_memcmp (coin_pub, &dc->cdd.coin_pub)) || (0 != strcmp (exchange_url, dc->exchange_url)) || (0 != TALER_amount_cmp (amount_with_fee, &dc->cdd.amount)) ) continue; /* does not match, skip */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit of coin `%s' already in our DB.\n", TALER_B2S (coin_pub)); GNUNET_assert (0 <= TALER_amount_add (&pc->total_paid, &pc->total_paid, amount_with_fee)); GNUNET_assert (0 <= TALER_amount_add (&pc->total_fees_paid, &pc->total_fees_paid, deposit_fee)); dc->deposit_fee = *deposit_fee; dc->refund_fee = *refund_fee; dc->wire_fee = *wire_fee; dc->cdd.amount = *amount_with_fee; dc->found_in_db = true; pc->pending--; } } /** * Function called with information about a refund. Check if this coin was * claimed by the wallet for the transaction, and if so add the refunded * amount to the pc's "total_refunded" amount. * * @param cls closure with a `struct PayContext` * @param coin_pub public coin from which the refund comes from * @param refund_amount refund amount which is being taken from @a coin_pub */ static void check_coin_refunded (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *refund_amount) { struct PayContext *pc = cls; /* We look at refunds here that apply to the coins that the customer is currently trying to pay us with. Such refunds are not "normal" refunds, but abort-pay refunds, which are given in the case that the wallet aborts the payment. In the case the wallet then decides to complete the payment *after* doing an abort-pay refund (an unusual but possible case), we need to make sure that existing refunds are accounted for. */ for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; /* Get matching coins from results. */ if (0 != GNUNET_memcmp (coin_pub, &dc->cdd.coin_pub)) continue; GNUNET_assert (0 <= TALER_amount_add (&pc->total_refunded, &pc->total_refunded, refund_amount)); break; } } /** * Check whether the amount paid is sufficient to cover the price. * * @param pc payment context to check * @return true if the payment is sufficient, false if it is * insufficient */ static bool check_payment_sufficient (struct PayContext *pc) { struct TALER_Amount acc_fee; struct TALER_Amount acc_amount; struct TALER_Amount final_amount; struct TALER_Amount wire_fee_delta; struct TALER_Amount wire_fee_customer_contribution; struct TALER_Amount total_wire_fee; struct TALER_Amount total_needed; if (0 == pc->coins_cnt) { return ((0 == pc->amount.value) && (0 == pc->amount.fraction)); } acc_fee = pc->dc[0].deposit_fee; total_wire_fee = pc->dc[0].wire_fee; acc_amount = pc->dc[0].cdd.amount; /** * This loops calculates what are the deposit fee / total * amount with fee / and wire fee, for all the coins. */ for (unsigned int i = 1; icoins_cnt; i++) { struct DepositConfirmation *dc = &pc->dc[i]; GNUNET_assert (dc->found_in_db); if ( (0 > TALER_amount_add (&acc_fee, &dc->deposit_fee, &acc_fee)) || (0 > TALER_amount_add (&acc_amount, &dc->cdd.amount, &acc_amount)) ) { GNUNET_break (0); /* Overflow in these amounts? Very strange. */ resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts"); return false; } if (1 == TALER_amount_cmp (&dc->deposit_fee, &dc->cdd.amount)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT, "Deposit fees exceed coin's contribution"); return false; } /* If exchange differs, add wire fee */ { bool new_exchange = true; for (unsigned int j = 0; jexchange_url, pc->dc[j].exchange_url)) { new_exchange = false; break; } if (! new_exchange) continue; if (GNUNET_OK != TALER_amount_cmp_currency (&total_wire_fee, &dc->wire_fee)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency); return false; } if (0 > TALER_amount_add (&total_wire_fee, &total_wire_fee, &dc->wire_fee)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, "could not add exchange wire fee to total"); return false; } } } /* deposit loop */ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Amount received from wallet: %s\n", TALER_amount2s (&acc_amount)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit fee for all coins: %s\n", TALER_amount2s (&acc_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Total wire fee: %s\n", TALER_amount2s (&total_wire_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Max wire fee: %s\n", TALER_amount2s (&pc->max_wire_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit fee limit for merchant: %s\n", TALER_amount2s (&pc->max_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Total refunded amount: %s\n", TALER_amount2s (&pc->total_refunded)); /* Now compare exchange wire fee compared to * what we are willing to pay */ if (GNUNET_YES != TALER_amount_cmp_currency (&total_wire_fee, &pc->max_wire_fee)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, total_wire_fee.currency); return false; } switch (TALER_amount_subtract (&wire_fee_delta, &total_wire_fee, &pc->max_wire_fee)) { case TALER_AAR_RESULT_POSITIVE: /* Actual wire fee is indeed higher than our maximum, compute how much the customer is expected to cover! */ TALER_amount_divide (&wire_fee_customer_contribution, &wire_fee_delta, pc->wire_fee_amortization); break; case TALER_AAR_RESULT_ZERO: case TALER_AAR_INVALID_NEGATIVE_RESULT: /* Wire fee threshold is still above the wire fee amount. Customer is not going to contribute on this. */ GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (total_wire_fee.currency, &wire_fee_customer_contribution)); break; default: GNUNET_assert (0); } /* add wire fee contribution to the total fees */ if (0 > TALER_amount_add (&acc_fee, &acc_fee, &wire_fee_customer_contribution)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts"); return false; } if (-1 == TALER_amount_cmp (&pc->max_fee, &acc_fee)) { /** * Sum of fees of *all* the different exchanges of all the coins are * higher than the fixed limit that the merchant is willing to pay. The * difference must be paid by the customer. */ struct TALER_Amount excess_fee; /* compute fee amount to be covered by customer */ GNUNET_assert (TALER_AAR_RESULT_POSITIVE == TALER_amount_subtract (&excess_fee, &acc_fee, &pc->max_fee)); /* add that to the total */ if (0 > TALER_amount_add (&total_needed, &excess_fee, &pc->amount)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW, "Overflow adding up amounts"); return false; } } else { /* Fees are fully covered by the merchant, all we require is that the total payment is not below the contract's amount */ total_needed = pc->amount; } /* Do not count refunds towards the payment */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Subtracting total refunds from paid amount: %s\n", TALER_amount2s (&pc->total_refunded)); if (0 > TALER_amount_subtract (&final_amount, &acc_amount, &pc->total_refunded)) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS, "refunded amount exceeds total payments"); return false; } if (-1 == TALER_amount_cmp (&final_amount, &total_needed)) { /* acc_amount < total_needed */ if (-1 < TALER_amount_cmp (&acc_amount, &total_needed)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_PAYMENT_REQUIRED, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED, "contract not paid up due to refunds"); } else if (-1 < TALER_amount_cmp (&acc_amount, &pc->amount)) { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_NOT_ACCEPTABLE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES, "contract not paid up due to fees (client may have calculated them badly)"); } else { GNUNET_break_op (0); resume_pay_with_error (pc, MHD_HTTP_NOT_ACCEPTABLE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT, "payment insufficient"); } return false; } return true; } /** * Use database to notify other clients about the * payment being completed. * * @param pc context to trigger notification for */ static void trigger_payment_notification (struct PayContext *pc) { { struct TMH_OrderPayEventP pay_eh = { .header.size = htons (sizeof (pay_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID), .merchant_pub = pc->hc->instance->merchant_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about payment of order %s\n", pc->order_id); GNUNET_CRYPTO_hash (pc->order_id, strlen (pc->order_id), &pay_eh.h_order_id); TMH_db->event_notify (TMH_db->cls, &pay_eh.header, NULL, 0); } if ( (NULL != pc->session_id) && (NULL != pc->fulfillment_url) ) { struct TMH_SessionEventP session_eh = { .header.size = htons (sizeof (session_eh)), .header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED), .merchant_pub = pc->hc->instance->merchant_pub }; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about session change to %s for %s\n", pc->session_id, pc->fulfillment_url); GNUNET_CRYPTO_hash (pc->session_id, strlen (pc->session_id), &session_eh.h_session_id); GNUNET_CRYPTO_hash (pc->fulfillment_url, strlen (pc->fulfillment_url), &session_eh.h_fulfillment_url); TMH_db->event_notify (TMH_db->cls, &session_eh.header, NULL, 0); } } /** * Generate response (payment successful) * * @param[in,out] pc payment context where the payment was successful */ static void generate_success_response (struct PayContext *pc) { struct GNUNET_CRYPTO_EddsaSignature sig; char *pos_confirmation; /* Sign on our end (as the payment did go through, even if it may have been refunded already) */ TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); /* Build the response */ pos_confirmation = (NULL == pc->pos_key) ? NULL : TALER_build_pos_confirmation (pc->pos_key, pc->pos_alg, &pc->amount, pc->timestamp); resume_pay_with_response ( pc, MHD_HTTP_OK, TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("pos_confirmation", pos_confirmation)), GNUNET_JSON_pack_data_auto ("sig", &sig))); GNUNET_free (pos_confirmation); } static void execute_pay_transaction (struct PayContext *pc) { struct TMH_HandlerContext *hc = pc->hc; const char *instance_id = hc->instance->settings.id; /* Avoid re-trying transactions on soft errors forever! */ if (pc->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); return; } GNUNET_assert (GNUNET_YES == pc->suspended); /* Initialize some amount accumulators (used in check_coin_paid(), check_coin_refunded() and check_payment_sufficient()). */ GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_paid)); GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_fees_paid)); GNUNET_break (GNUNET_OK == TALER_amount_set_zero (pc->amount.currency, &pc->total_refunded)); for (unsigned int i = 0; icoins_cnt; i++) pc->dc[i].found_in_db = false; pc->pending = pc->coins_cnt; /* First, try to see if we have all we need already done */ TMH_db->preflight (TMH_db->cls); if (GNUNET_OK != TMH_db->start (TMH_db->cls, "run pay")) { GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL); return; } { enum GNUNET_DB_QueryStatus qs; /* Check if some of these coins already succeeded for _this_ contract. */ qs = TMH_db->lookup_deposits (TMH_db->cls, instance_id, &pc->h_contract_terms, &check_coin_paid, pc); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup deposits"); return; } } { enum GNUNET_DB_QueryStatus qs; /* Check if we refunded some of the coins */ qs = TMH_db->lookup_refunds (TMH_db->cls, instance_id, &pc->h_contract_terms, &check_coin_refunded, pc); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup refunds"); return; } } /* Check if there are coins that still need to be processed */ if (0 != pc->pending) { /* we made no DB changes, so we can just rollback */ TMH_db->rollback (TMH_db->cls); /* Ok, we need to first go to the network to process more coins. We that interaction in *tiny* transactions (hence the rollback above). */ start_batch_deposits (pc); return; } /* 0 == pc->pending: all coins processed, let's see if that was enough */ if (! check_payment_sufficient (pc)) { /* check_payment_sufficient() will have queued an error already. We need to still abort the transaction. */ TMH_db->rollback (TMH_db->cls); return; } /* Payment succeeded, save in database */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' (%s) was fully paid\n", pc->order_id, GNUNET_h2s (&pc->h_contract_terms.hash)); { enum GNUNET_DB_QueryStatus qs; qs = TMH_db->mark_contract_paid (TMH_db->cls, instance_id, &pc->h_contract_terms, pc->session_id); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "mark contract paid"); return; } } TMH_notify_order_change (hc->instance, TMH_OSF_CLAIMED | TMH_OSF_PAID, pc->timestamp, pc->order_serial); { enum GNUNET_DB_QueryStatus qs; json_t *jhook; jhook = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_incref ("contract_terms", pc->contract_terms), GNUNET_JSON_pack_string ("order_id", pc->order_id) ); GNUNET_assert (NULL != jhook); qs = TMH_trigger_webhook (pc->hc->instance->settings.id, "pay", jhook); json_decref (jhook); if (qs < 0) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "failed to trigger webhooks"); return; } } { enum GNUNET_DB_QueryStatus qs; /* Now commit! */ qs = TMH_db->commit (TMH_db->cls); if (0 > qs) { /* commit failed */ TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { execute_pay_transaction (pc); return; } GNUNET_break (0); resume_pay_with_error (pc, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); return; } trigger_payment_notification (pc); } generate_success_response (pc); } /** * Try to parse the pay request into the given pay context. * Schedules an error response in the connection on failure. * * @param[in,out] pc context we use to handle the payment * @return #GNUNET_OK on success, * #GNUNET_NO on failure (response was queued with MHD) * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ static enum GNUNET_GenericReturnValue parse_pay (struct PayContext *pc) { const char *session_id = NULL; const json_t *coins; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("coins", &coins), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("session_id", &session_id), NULL), GNUNET_JSON_spec_end () }; { enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (pc->connection, pc->hc->request_body, spec); if (GNUNET_YES != res) { GNUNET_break_op (0); return res; } } /* copy session ID (if set) */ if (NULL != session_id) { pc->session_id = GNUNET_strdup (session_id); } else { /* use empty string as default if client didn't specify it */ pc->session_id = GNUNET_strdup (""); } pc->coins_cnt = json_array_size (coins); if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS) { GNUNET_break_op (0); return (MHD_YES == TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "'coins' array too long")) ? GNUNET_NO : GNUNET_SYSERR; } /* note: 1 coin = 1 deposit confirmation expected */ pc->dc = GNUNET_new_array (pc->coins_cnt, struct DepositConfirmation); /* This loop populates the array 'dc' in 'pc' */ { unsigned int coins_index; json_t *coin; json_array_foreach (coins, coins_index, coin) { struct DepositConfirmation *dc = &pc->dc[coins_index]; const char *exchange_url; struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_fixed_auto ("coin_sig", &dc->cdd.coin_sig), GNUNET_JSON_spec_fixed_auto ("coin_pub", &dc->cdd.coin_pub), TALER_JSON_spec_denom_sig ("ub_sig", &dc->cdd.denom_sig), GNUNET_JSON_spec_fixed_auto ("h_denom", &dc->cdd.h_denom_pub), TALER_JSON_spec_amount ("contribution", TMH_currency, &dc->cdd.amount), GNUNET_JSON_spec_string ("exchange_url", &exchange_url), /* if a minimum age was required, the minimum_age_sig and * age_commitment must be provided */ GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", &dc->minimum_age_sig), &dc->no_minimum_age_sig), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_age_commitment ("age_commitment", &dc->age_commitment), &dc->no_age_commitment), /* if minimum age was not required, but coin with age restriction set * was used, h_age_commitment must be provided. */ GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("h_age_commitment", &dc->cdd.h_age_commitment), &dc->no_h_age_commitment), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; bool have_eg = false; res = TALER_MHD_parse_json_data (pc->connection, coin, ispec); if (GNUNET_YES != res) { GNUNET_break_op (0); return res; } for (unsigned int j = 0; jcdd.coin_pub, &pc->dc[j].cdd.coin_pub)) { GNUNET_break_op (0); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "duplicate coin in list")) ? GNUNET_NO : GNUNET_SYSERR; } } dc->exchange_url = GNUNET_strdup (exchange_url); dc->index = coins_index; dc->pc = pc; if (0 != strcasecmp (dc->cdd.amount.currency, TMH_currency)) { GNUNET_break_op (0); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, TMH_currency)) ? GNUNET_NO : GNUNET_SYSERR; } /* Check the consistency of the (potential) age restriction * information. */ if (dc->no_age_commitment != dc->no_minimum_age_sig) { GNUNET_break_op (0); return (MHD_YES == TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "inconsistent: 'age_commitment' vs. 'minimum_age_sig'" ) ) ? GNUNET_NO : GNUNET_SYSERR; } /* Setup exchange group */ for (unsigned int i = 0; inum_exchanges; i++) { if (0 == strcmp (pc->egs[i]->exchange_url, exchange_url)) { have_eg = true; break; } } if (! have_eg) { struct ExchangeGroup *eg; eg = GNUNET_new (struct ExchangeGroup); eg->pc = pc; eg->exchange_url = dc->exchange_url; GNUNET_array_append (pc->egs, pc->num_exchanges, eg); } } } return GNUNET_OK; } /** * Function called with information about a coin that was deposited. * Checks if this coin is in our list of deposits as well. * * @param cls closure with our `struct PayContext *` * @param deposit_serial which deposit operation is this about * @param exchange_url URL of the exchange that issued the coin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param h_wire hash of merchant's wire details * @param coin_pub public key of the coin */ static void deposit_paid_check ( void *cls, uint64_t deposit_serial, const char *exchange_url, const struct TALER_MerchantWireHashP *h_wire, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct PayContext *pc = cls; for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if ( (0 == GNUNET_memcmp (&dci->cdd.coin_pub, coin_pub)) && (0 == strcmp (dci->exchange_url, exchange_url)) && (0 == TALER_amount_cmp (&dci->cdd.amount, amount_with_fee)) ) { dci->matched_in_db = true; break; } } } /** * Handle case where contract was already paid. Either decides * the payment is idempotent, or refunds the excess payment. * * @param[in,out] pc context we use to handle the payment * @return #GNUNET_NO if response was queued with MHD * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ static enum GNUNET_GenericReturnValue handle_contract_paid (struct PayContext *pc) { enum GNUNET_DB_QueryStatus qs; bool unmatched = false; json_t *refunds; qs = TMH_db->lookup_deposits_by_order (TMH_db->cls, pc->order_serial, &deposit_paid_check, pc); if (qs <= 0) { GNUNET_break (0); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "lookup_deposits_by_order")) ? GNUNET_NO : GNUNET_SYSERR; } for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; if (! dci->matched_in_db) unmatched = true; } if (! unmatched) { /* Everything fine, idempotent request */ struct GNUNET_CRYPTO_EddsaSignature sig; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Idempotent pay request for order `%s', signing again\n", pc->order_id); TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); return (MHD_YES == TALER_MHD_REPLY_JSON_PACK ( pc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_data_auto ("sig", &sig))) ? GNUNET_NO : GNUNET_SYSERR; } /* Conflict, double-payment detected! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client attempted to pay extra for already paid order `%s'\n", pc->order_id); refunds = json_array (); GNUNET_assert (NULL != refunds); for (unsigned int i = 0; icoins_cnt; i++) { struct DepositConfirmation *dci = &pc->dc[i]; struct TALER_MerchantSignatureP merchant_sig; if (dci->matched_in_db) continue; TALER_merchant_refund_sign (&dci->cdd.coin_pub, &pc->h_contract_terms, 0, /* rtransaction id */ &dci->cdd.amount, &pc->hc->instance->merchant_priv, &merchant_sig); GNUNET_assert ( 0 == json_array_append_new ( refunds, GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ( "coin_pub", &dci->cdd.coin_pub), GNUNET_JSON_pack_data_auto ( "merchant_sig", &merchant_sig), TALER_JSON_pack_amount ("amount", &dci->cdd.amount), GNUNET_JSON_pack_uint64 ("rtransaction_id", 0)))); } return (MHD_YES == TALER_MHD_REPLY_JSON_PACK ( pc->connection, MHD_HTTP_CONFLICT, TALER_MHD_PACK_EC ( TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID), GNUNET_JSON_pack_array_steal ("refunds", refunds))) ? GNUNET_NO : GNUNET_SYSERR; } /** * Check the database state for the given order. * Schedules an error response in the connection on failure. * * @param pc context we use to handle the payment * @return #GNUNET_OK on success, * #GNUNET_NO on failure (response was queued with MHD) * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ static enum GNUNET_GenericReturnValue check_contract (struct PayContext *pc) { /* obtain contract terms */ enum GNUNET_DB_QueryStatus qs; bool paid = false; if (NULL != pc->contract_terms) { json_decref (pc->contract_terms); pc->contract_terms = NULL; } qs = TMH_db->lookup_contract_terms2 (TMH_db->cls, pc->hc->instance->settings.id, pc->order_id, &pc->contract_terms, &pc->order_serial, &paid, NULL, &pc->pos_key, &pc->pos_alg); if (0 > qs) { /* single, read-only SQL statements should never cause serialization problems */ GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); /* Always report on hard error to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "contract terms")) ? GNUNET_NO : GNUNET_SYSERR; } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN, pc->order_id)) ? GNUNET_NO : GNUNET_SYSERR; } /* hash contract (needed later) */ if (GNUNET_OK != TALER_JSON_contract_hash (pc->contract_terms, &pc->h_contract_terms)) { GNUNET_break (0); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } if (paid) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' paid, checking for double-payment\n", pc->order_id); return handle_contract_paid (pc); } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling payment for order `%s' with contract hash `%s'\n", pc->order_id, GNUNET_h2s (&pc->h_contract_terms.hash)); /* basic sanity check on the contract */ if (NULL == json_object_get (pc->contract_terms, "merchant")) { /* invalid contract */ GNUNET_break (0); return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } /* Get details from contract and check fundamentals */ { const char *fulfillment_url = NULL; struct GNUNET_JSON_Specification espec[] = { TALER_JSON_spec_amount ("amount", TMH_currency, &pc->amount), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url), NULL), TALER_JSON_spec_amount ("max_fee", TMH_currency, &pc->max_fee), TALER_JSON_spec_amount ("max_wire_fee", TMH_currency, &pc->max_wire_fee), GNUNET_JSON_spec_uint32 ("wire_fee_amortization", &pc->wire_fee_amortization), GNUNET_JSON_spec_timestamp ("timestamp", &pc->timestamp), GNUNET_JSON_spec_timestamp ("refund_deadline", &pc->refund_deadline), GNUNET_JSON_spec_timestamp ("pay_deadline", &pc->pay_deadline), GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", &pc->wire_transfer_deadline), GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("minimum_age", &pc->minimum_age), NULL), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; pc->minimum_age = 0; res = TALER_MHD_parse_internal_json_data (pc->connection, pc->contract_terms, espec); if (NULL != fulfillment_url) pc->fulfillment_url = GNUNET_strdup (fulfillment_url); if (GNUNET_YES != res) { GNUNET_break (0); return res; } } if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline, <, pc->refund_deadline)) { /* This should already have been checked when creating the order! */ GNUNET_break (0); return TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, NULL); } if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time)) { /* too late */ return (MHD_YES == TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_GONE, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED, NULL)) ? GNUNET_NO : GNUNET_SYSERR; } /* Make sure wire method (still) exists for this instance */ { struct TMH_WireMethod *wm; wm = pc->hc->instance->wm_head; while (0 != GNUNET_memcmp (&pc->h_wire, &wm->h_wire)) wm = wm->next; if (NULL == wm) { GNUNET_break (0); return TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN, NULL); } pc->wm = wm; } return GNUNET_OK; } /** * Handle a timeout for the processing of the pay request. * * @param cls our `struct PayContext` */ static void handle_pay_timeout (void *cls) { struct PayContext *pc = cls; pc->timeout_task = NULL; GNUNET_assert (GNUNET_YES == pc->suspended); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Resuming pay with error after timeout\n"); resume_pay_with_error (pc, MHD_HTTP_GATEWAY_TIMEOUT, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); } MHD_RESULT TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct PayContext *pc = hc->ctx; enum GNUNET_GenericReturnValue ret; GNUNET_assert (NULL != hc->infix); if (NULL == pc) { pc = GNUNET_new (struct PayContext); GNUNET_CONTAINER_DLL_insert (pc_head, pc_tail, pc); pc->connection = connection; pc->hc = hc; pc->order_id = hc->infix; hc->ctx = pc; hc->cc = &pay_context_cleanup; ret = parse_pay (pc); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } if (GNUNET_SYSERR == pc->suspended) return MHD_NO; /* during shutdown, we don't generate any more replies */ GNUNET_assert (GNUNET_NO == pc->suspended); if (0 != pc->response_code) { /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == pc->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } return MHD_queue_response (connection, pc->response_code, pc->response); } ret = check_contract (pc); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; /* Payment not finished, suspend while we interact with the exchange */ MHD_suspend_connection (connection); pc->suspended = GNUNET_YES; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending pay handling while working with the exchange\n"); GNUNET_assert (NULL == pc->timeout_task); pc->timeout_task = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt), &handle_pay_timeout, pc); GNUNET_assert (NULL != pc->wm); execute_pay_transaction (pc); return MHD_YES; } /* end of taler-merchant-httpd_post-orders-ID-pay.c */