/* This file is part of TALER (C) 2014-2021 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, see */ /** * @file taler-merchant-httpd_post-orders-ID-abort.c * @brief handling of POST /orders/$ID/abort requests * @author Marcello Stanisci * @author Christian Grothoff * @author Florian Dold */ #include "platform.h" #include #include #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_post-orders-ID-abort.h" /** * How long to wait before giving up processing with the exchange? */ #define ABORT_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, \ 30)) /** * How often do we retry the (complex!) database transaction? */ #define MAX_RETRIES 5 /** * Information we keep for an individual call to the /abort handler. */ struct AbortContext; /** * Information kept during a /abort request for each coin. */ struct RefundDetails { /** * Public key of the coin. */ struct TALER_CoinSpendPublicKeyP coin_pub; /** * Signature from the exchange confirming the refund. * Set if we were successful (status 200). */ struct TALER_ExchangeSignatureP exchange_sig; /** * Public key used for @e exchange_sig. * Set if we were successful (status 200). */ struct TALER_ExchangePublicKeyP exchange_pub; /** * Reference to the main AbortContext */ struct AbortContext *ac; /** * Handle to the refund operation we are performing for * this coin, NULL after the operation is done. */ struct TALER_EXCHANGE_RefundHandle *rh; /** * URL of the exchange that issued this coin. */ char *exchange_url; /** * Body of the response from the exchange. Note that the body returned MUST * be freed (if non-NULL). */ json_t *exchange_reply; /** * Amount this coin contributes to the total purchase price. * This amount includes the deposit fee. */ struct TALER_Amount amount_with_fee; /** * Offset of this coin into the `rd` array of all coins in the * @e ac. */ unsigned int index; /** * HTTP status returned by the exchange (if any). */ unsigned int http_status; /** * Did we try to process this refund yet? */ bool processed; }; /** * Information we keep for an individual call to the /abort handler. */ struct AbortContext { /** * Hashed contract terms (according to client). */ struct TALER_PrivateContractHashP h_contract_terms; /** * Context for our operation. */ struct TMH_HandlerContext *hc; /** * Stored in a DLL. */ struct AbortContext *next; /** * Stored in a DLL. */ struct AbortContext *prev; /** * Array with @e coins_cnt coins we are despositing. */ struct RefundDetails *rd; /** * MHD connection to return to */ struct MHD_Connection *connection; /** * Task called when the (suspended) processing for * the /abort request times out. * Happens when we don't get a response from the exchange. */ struct GNUNET_SCHEDULER_Task *timeout_task; /** * Response to return, NULL if we don't have one yet. */ struct MHD_Response *response; /** * Handle to the exchange that we are doing the abortment with. * (initially NULL while @e fo is trying to find a exchange). */ struct TALER_EXCHANGE_Handle *mh; /** * Handle for operation to lookup /keys (and auditors) from * the exchange used for this transaction; NULL if no operation is * pending. */ struct TMH_EXCHANGES_KeysOperation *fo; /** * URL of the exchange used for the last @e fo. */ const char *current_exchange; /** * Number of coins this abort is for. Length of the @e rd array. */ size_t 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. */ size_t 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. */ size_t pending_at_ce; /** * HTTP status code to use for the reply, i.e 200 for "OK". * Special value UINT_MAX is used to indicate hard errors * (no reply, return #MHD_NO). */ unsigned int response_code; /** * #GNUNET_NO if the @e connection was not suspended, * #GNUNET_YES if the @e connection was suspended, * #GNUNET_SYSERR if @e connection was resumed to as * part of #MH_force_ac_resume during shutdown. */ int suspended; }; /** * Head of active abort context DLL. */ static struct AbortContext *ac_head; /** * Tail of active abort context DLL. */ static struct AbortContext *ac_tail; /** * Abort all pending /deposit operations. * * @param ac abort context to abort */ static void abort_refunds (struct AbortContext *ac) { GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Aborting pending /deposit operations\n"); for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; if (NULL != rdi->rh) { TALER_EXCHANGE_refund_cancel (rdi->rh); rdi->rh = NULL; } } } void TMH_force_ac_resume () { for (struct AbortContext *ac = ac_head; NULL != ac; ac = ac->next) { abort_refunds (ac); if (NULL != ac->timeout_task) { GNUNET_SCHEDULER_cancel (ac->timeout_task); ac->timeout_task = NULL; } if (GNUNET_YES == ac->suspended) { ac->suspended = GNUNET_SYSERR; MHD_resume_connection (ac->connection); } } } /** * Resume the given abort context and send the given response. * Stores the response in the @a ac and signals MHD to resume * the connection. Also ensures MHD runs immediately. * * @param ac abortment context * @param response_code response code to use * @param response response data to send back */ static void resume_abort_with_response (struct AbortContext *ac, unsigned int response_code, struct MHD_Response *response) { abort_refunds (ac); ac->response_code = response_code; ac->response = response; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /abort handling as exchange interaction is done (%u)\n", response_code); if (NULL != ac->timeout_task) { GNUNET_SCHEDULER_cancel (ac->timeout_task); ac->timeout_task = NULL; } GNUNET_assert (GNUNET_YES == ac->suspended); ac->suspended = GNUNET_NO; MHD_resume_connection (ac->connection); TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ } /** * Resume abortment processing with an error. * * @param ac 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_abort_with_error (struct AbortContext *ac, unsigned int http_status, enum TALER_ErrorCode ec, const char *msg) { resume_abort_with_response (ac, http_status, TALER_MHD_make_error (ec, msg)); } /** * Generate a response that indicates abortment success. * * @param ac abortment context */ static void generate_success_response (struct AbortContext *ac) { json_t *refunds; unsigned int hc = MHD_HTTP_OK; refunds = json_array (); if (NULL == refunds) { GNUNET_break (0); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, "could not create JSON array"); return; } for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; json_t *detail; if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) && (MHD_HTTP_NOT_FOUND != rdi->http_status) && (MHD_HTTP_GONE != rdi->http_status) ) || (0 == rdi->http_status) || (NULL == rdi->exchange_reply) ) hc = MHD_HTTP_BAD_GATEWAY; if (MHD_HTTP_OK != rdi->http_status) detail = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "failure"), GNUNET_JSON_pack_uint64 ("exchange_status", rdi->http_status), GNUNET_JSON_pack_uint64 ("exchange_code", (NULL != rdi->exchange_reply) ? TALER_JSON_get_error_code ( rdi->exchange_reply) : TALER_EC_GENERIC_INVALID_RESPONSE), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("exchange_reply", rdi->exchange_reply))); else detail = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "success"), GNUNET_JSON_pack_uint64 ("exchange_status", rdi->http_status), GNUNET_JSON_pack_data_auto ("exchange_sig", &rdi->exchange_sig), GNUNET_JSON_pack_data_auto ("exchange_pub", &rdi->exchange_pub)); GNUNET_assert (0 == json_array_append_new (refunds, detail)); } /* Resume and send back the response. */ resume_abort_with_response ( ac, hc, TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_array_steal ("refunds", refunds))); } /** * Custom cleanup routine for a `struct AbortContext`. * * @param cls the `struct AbortContext` to clean up. */ static void abort_context_cleanup (void *cls) { struct AbortContext *ac = cls; if (NULL != ac->timeout_task) { GNUNET_SCHEDULER_cancel (ac->timeout_task); ac->timeout_task = NULL; } abort_refunds (ac); for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; if (NULL != rdi->exchange_reply) { json_decref (rdi->exchange_reply); rdi->exchange_reply = NULL; } GNUNET_free (rdi->exchange_url); } GNUNET_free (ac->rd); if (NULL != ac->fo) { TMH_EXCHANGES_keys4exchange_cancel (ac->fo); ac->fo = NULL; } if (NULL != ac->response) { MHD_destroy_response (ac->response); ac->response = NULL; } GNUNET_CONTAINER_DLL_remove (ac_head, ac_tail, ac); GNUNET_free (ac); } /** * Find the exchange we need to talk to for the next * pending deposit permission. * * @param ac abortment context we are processing */ static void find_next_exchange (struct AbortContext *ac); /** * Function called with the result from the exchange (to be * passed back to the wallet). * * @param cls closure * @param rr response data */ static void refund_cb (void *cls, const struct TALER_EXCHANGE_RefundResponse *rr) { struct RefundDetails *rd = cls; const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr; struct AbortContext *ac = rd->ac; rd->rh = NULL; rd->http_status = hr->http_status; rd->exchange_reply = json_incref ((json_t*) hr->reply); if (MHD_HTTP_OK == hr->http_status) { rd->exchange_pub = rr->details.ok.exchange_pub; rd->exchange_sig = rr->details.ok.exchange_sig; } ac->pending_at_ce--; if (0 == ac->pending_at_ce) find_next_exchange (ac); } /** * Function called with the result of our exchange lookup. * * @param cls the `struct AbortContext` * @param keys keys of the exchange * @param exchange representation of the exchange */ static void process_abort_with_exchange (void *cls, struct TALER_EXCHANGE_Keys *keys, struct TMH_Exchange *exchange) { struct AbortContext *ac = cls; (void) exchange; ac->fo = NULL; GNUNET_assert (GNUNET_YES == ac->suspended); if (NULL == keys) { resume_abort_with_response ( ac, MHD_HTTP_GATEWAY_TIMEOUT, TALER_MHD_make_error ( TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL)); return; } /* Initiate refund operation for all coins of the current exchange (!) */ GNUNET_assert (0 == ac->pending_at_ce); for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; if (rdi->processed) continue; GNUNET_assert (NULL == rdi->rh); if (0 != strcmp (rdi->exchange_url, ac->current_exchange)) continue; rdi->processed = true; ac->pending--; rdi->rh = TALER_EXCHANGE_refund ( TMH_curl_ctx, ac->current_exchange, keys, &rdi->amount_with_fee, &ac->h_contract_terms, &rdi->coin_pub, 0, /* rtransaction_id */ &ac->hc->instance->merchant_priv, &refund_cb, rdi); if (NULL == rdi->rh) { GNUNET_break_op (0); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED, "Failed to start refund with exchange"); return; } ac->pending_at_ce++; } } /** * Begin of the DB transaction. If required (from * soft/serialization errors), the transaction can be * restarted here. * * @param ac abortment context to transact */ static void begin_transaction (struct AbortContext *ac); /** * Find the exchange we need to talk to for the next * pending deposit permission. * * @param ac abortment context we are processing */ static void find_next_exchange (struct AbortContext *ac) { for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; if (! rdi->processed) { ac->current_exchange = rdi->exchange_url; ac->fo = TMH_EXCHANGES_keys4exchange (ac->current_exchange, false, &process_abort_with_exchange, ac); if (NULL == ac->fo) { /* strange, should have happened on pay! */ GNUNET_break (0); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED, ac->current_exchange); return; } return; } } ac->current_exchange = NULL; GNUNET_assert (0 == ac->pending); /* We are done with all the HTTP requests, go back and try the 'big' database transaction! (It should work now!) */ begin_transaction (ac); } /** * Function called with information about a coin that was deposited. * * @param cls closure * @param exchange_url exchange where @a coin_pub was deposited * @param coin_pub public key of the coin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin */ static void refund_coins (void *cls, const char *exchange_url, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, const struct TALER_Amount *refund_fee) { struct AbortContext *ac = cls; struct GNUNET_TIME_Timestamp now; (void) amount_with_fee; (void) deposit_fee; (void) refund_fee; now = GNUNET_TIME_timestamp_get (); for (size_t i = 0; icoins_cnt; i++) { struct RefundDetails *rdi = &ac->rd[i]; enum GNUNET_DB_QueryStatus qs; if ( (0 != GNUNET_memcmp (coin_pub, &rdi->coin_pub)) || (0 != strcmp (exchange_url, rdi->exchange_url)) ) continue; /* not in request */ /* Store refund in DB */ qs = TMH_db->refund_coin (TMH_db->cls, ac->hc->instance->settings.id, &ac->h_contract_terms, now, coin_pub, /* justification */ "incomplete abortment aborted"); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, "refund_coin"); return; } } /* for all coins */ } /** * Begin of the DB transaction. If required (from soft/serialization errors), * the transaction can be restarted here. * * @param ac abortment context to transact */ static void begin_transaction (struct AbortContext *ac) { enum GNUNET_DB_QueryStatus qs; /* Avoid re-trying transactions on soft errors forever! */ if (ac->retry_counter++ > MAX_RETRIES) { GNUNET_break (0); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_SOFT_FAILURE, NULL); return; } GNUNET_assert (GNUNET_YES == ac->suspended); /* 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 abort")) { GNUNET_break (0); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_START_FAILED, NULL); return; } /* check payment was indeed incomplete (now that we are in the transaction scope!) */ { struct TALER_PrivateContractHashP h_contract_terms; bool paid; qs = TMH_db->lookup_order_status (TMH_db->cls, ac->hc->instance->settings.id, ac->hc->infix, &h_contract_terms, &paid); switch (qs) { case GNUNET_DB_STATUS_SOFT_ERROR: case GNUNET_DB_STATUS_HARD_ERROR: /* Always report on hard error to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "order status"); return; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: TMH_db->rollback (TMH_db->cls); resume_abort_with_error (ac, MHD_HTTP_NOT_FOUND, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND, "Could not find contract"); return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: if (paid) { /* Payment is complete, refuse to abort. */ TMH_db->rollback (TMH_db->cls); resume_abort_with_error (ac, MHD_HTTP_PRECONDITION_FAILED, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, "Payment was complete, refusing to abort"); return; } } if (0 != GNUNET_memcmp (&ac->h_contract_terms, &h_contract_terms)) { GNUNET_break_op (0); resume_abort_with_error (ac, MHD_HTTP_FORBIDDEN, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH, "Provided hash does not match order on file"); return; } } /* Mark all deposits we have in our database for the order as refunded. */ qs = TMH_db->lookup_deposits (TMH_db->cls, ac->hc->instance->settings.id, &ac->h_contract_terms, &refund_coins, ac); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); return; } /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "deposits"); return; } qs = TMH_db->commit (TMH_db->cls); if (0 > qs) { TMH_db->rollback (TMH_db->cls); if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { begin_transaction (ac); return; } resume_abort_with_error (ac, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_COMMIT_FAILED, NULL); return; } /* At this point, the refund got correctly committed into the database. Tell exchange about abort/refund. */ if (ac->pending > 0) { find_next_exchange (ac); return; } generate_success_response (ac); } /** * Try to parse the abort request into the given abort context. * Schedules an error response in the connection on failure. * * @param connection HTTP connection we are receiving abortment on * @param hc context we use to handle the abortment * @param ac state of the /abort call * @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_abort (struct MHD_Connection *connection, struct TMH_HandlerContext *hc, struct AbortContext *ac) { const json_t *coins; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_array_const ("coins", &coins), GNUNET_JSON_spec_fixed_auto ("h_contract", &ac->h_contract_terms), GNUNET_JSON_spec_end () }; enum GNUNET_GenericReturnValue res; res = TALER_MHD_parse_json_data (connection, hc->request_body, spec); if (GNUNET_YES != res) { GNUNET_break_op (0); return res; } ac->coins_cnt = json_array_size (coins); if (0 == ac->coins_cnt) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY, "coins"); } /* note: 1 coin = 1 deposit confirmation expected */ ac->pending = ac->coins_cnt; ac->rd = GNUNET_new_array (ac->coins_cnt, struct RefundDetails); /* This loop populates the array 'rd' in 'ac' */ { unsigned int coins_index; json_t *coin; json_array_foreach (coins, coins_index, coin) { struct RefundDetails *rd = &ac->rd[coins_index]; const char *exchange_url; struct GNUNET_JSON_Specification ispec[] = { TALER_JSON_spec_amount ("contribution", TMH_currency, &rd->amount_with_fee), TALER_JSON_spec_web_url ("exchange_url", &exchange_url), GNUNET_JSON_spec_fixed_auto ("coin_pub", &rd->coin_pub), GNUNET_JSON_spec_end () }; res = TALER_MHD_parse_json_data (connection, coin, ispec); if (GNUNET_YES != res) { GNUNET_break_op (0); return res; } rd->exchange_url = GNUNET_strdup (exchange_url); rd->index = coins_index; rd->ac = ac; } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Handling /abort for order `%s' with contract hash `%s'\n", ac->hc->infix, GNUNET_h2s (&ac->h_contract_terms.hash)); return GNUNET_OK; } /** * Handle a timeout for the processing of the abort request. * * @param cls our `struct AbortContext` */ static void handle_abort_timeout (void *cls) { struct AbortContext *ac = cls; ac->timeout_task = NULL; GNUNET_assert (GNUNET_YES == ac->suspended); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming abort with error after timeout\n"); if (NULL != ac->fo) { TMH_EXCHANGES_keys4exchange_cancel (ac->fo); ac->fo = NULL; } resume_abort_with_error (ac, MHD_HTTP_GATEWAY_TIMEOUT, TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT, NULL); } MHD_RESULT TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { struct AbortContext *ac = hc->ctx; if (NULL == ac) { ac = GNUNET_new (struct AbortContext); GNUNET_CONTAINER_DLL_insert (ac_head, ac_tail, ac); ac->connection = connection; ac->hc = hc; hc->ctx = ac; hc->cc = &abort_context_cleanup; } if (GNUNET_SYSERR == ac->suspended) return MHD_NO; /* during shutdown, we don't generate any more replies */ if (0 != ac->response_code) { MHD_RESULT res; /* We are *done* processing the request, just queue the response (!) */ if (UINT_MAX == ac->response_code) { GNUNET_break (0); return MHD_NO; /* hard error */ } res = MHD_queue_response (connection, ac->response_code, ac->response); MHD_destroy_response (ac->response); ac->response = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Queueing response (%u) for /abort (%s).\n", (unsigned int) ac->response_code, res ? "OK" : "FAILED"); return res; } { enum GNUNET_GenericReturnValue ret; ret = parse_abort (connection, hc, ac); if (GNUNET_OK != ret) return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; } /* Abort not finished, suspend while we interact with the exchange */ GNUNET_assert (GNUNET_NO == ac->suspended); MHD_suspend_connection (connection); ac->suspended = GNUNET_YES; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Suspending abort handling while working with the exchange\n"); ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_GENERIC_TIMEOUT, &handle_abort_timeout, ac); begin_transaction (ac); return MHD_YES; } /* end of taler-merchant-httpd_post-orders-ID-abort.c */