diff options
-rw-r--r-- | src/backend/taler-merchant-httpd_get-orders.c (renamed from src/backend/taler-merchant-httpd_history.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-reserves-reserve.c (renamed from src/backend/taler-merchant-httpd_tip-query.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-reserves.c (renamed from src/backend/taler-merchant-httpd_tip-reserve-helper.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-tips-tip.c (renamed from src/backend/taler-merchant-httpd_tip-pickup_get.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_get-transfers.c (renamed from src/backend/taler-merchant-httpd_track-transfer.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_get.h (renamed from src/backend/taler-merchant-httpd_history.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_abort.h (renamed from src/backend/taler-merchant-httpd_pay.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get.c (renamed from src/backend/taler-merchant-httpd_poll-payment.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get.h (renamed from src/backend/taler-merchant-httpd_poll-payment.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get2.c (renamed from src/backend/taler-merchant-httpd_refund_lookup.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get2.h (renamed from src/backend/taler-merchant-httpd_refund_lookup.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get3.c (renamed from src/backend/taler-merchant-httpd_track-transaction.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get4.c (renamed from src/backend/taler-merchant-httpd_check-payment.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get5.c (renamed from src/backend/taler-merchant-httpd_proposal.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_get5.h (renamed from src/backend/taler-merchant-httpd_proposal.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_pay.c (renamed from src/backend/taler-merchant-httpd_pay.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_pay.h | 54 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_refund.c (renamed from src/backend/taler-merchant-httpd_refund_increase.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_order_refund.h (renamed from src/backend/taler-merchant-httpd_refund_increase.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_orders_post.h (renamed from src/backend/taler-merchant-httpd_order.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders-order-abort.c | 2255 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-orders.c (renamed from src/backend/taler-merchant-httpd_order.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-tips-tip-pickup.c (renamed from src/backend/taler-merchant-httpd_tip-pickup.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-tips.c (renamed from src/backend/taler-merchant-httpd_tip-authorize.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_post-transfers.c | 1089 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_reserves_get.h (renamed from src/backend/taler-merchant-httpd_tip-reserve-helper.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_reserves_reserve_get.h (renamed from src/backend/taler-merchant-httpd_tip-query.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_responses.c (renamed from src/backend/taler-merchant-httpd_refund.c) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tips_post.h (renamed from src/backend/taler-merchant-httpd_tip-authorize.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_tips_tip_pickup.h (renamed from src/backend/taler-merchant-httpd_tip-pickup.h) | 0 | ||||
-rw-r--r-- | src/backend/taler-merchant-httpd_transfers-post.h (renamed from src/backend/taler-merchant-httpd_track-transfer.h) | 0 | ||||
-rw-r--r-- | src/backenddb/merchant-0001.sql | 331 | ||||
-rw-r--r-- | src/merchant-tools/taler-merchant-benchmark.c | 2 |
33 files changed, 3541 insertions, 190 deletions
diff --git a/src/backend/taler-merchant-httpd_history.c b/src/backend/taler-merchant-httpd_get-orders.c index dd353208..dd353208 100644 --- a/src/backend/taler-merchant-httpd_history.c +++ b/src/backend/taler-merchant-httpd_get-orders.c diff --git a/src/backend/taler-merchant-httpd_tip-query.c b/src/backend/taler-merchant-httpd_get-reserves-reserve.c index f7aa0ab0..f7aa0ab0 100644 --- a/src/backend/taler-merchant-httpd_tip-query.c +++ b/src/backend/taler-merchant-httpd_get-reserves-reserve.c diff --git a/src/backend/taler-merchant-httpd_tip-reserve-helper.c b/src/backend/taler-merchant-httpd_get-reserves.c index e104e089..e104e089 100644 --- a/src/backend/taler-merchant-httpd_tip-reserve-helper.c +++ b/src/backend/taler-merchant-httpd_get-reserves.c diff --git a/src/backend/taler-merchant-httpd_tip-pickup_get.c b/src/backend/taler-merchant-httpd_get-tips-tip.c index 42066e3c..42066e3c 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup_get.c +++ b/src/backend/taler-merchant-httpd_get-tips-tip.c diff --git a/src/backend/taler-merchant-httpd_track-transfer.c b/src/backend/taler-merchant-httpd_get-transfers.c index 7f55c917..7f55c917 100644 --- a/src/backend/taler-merchant-httpd_track-transfer.c +++ b/src/backend/taler-merchant-httpd_get-transfers.c diff --git a/src/backend/taler-merchant-httpd_history.h b/src/backend/taler-merchant-httpd_orders_get.h index eac987dd..eac987dd 100644 --- a/src/backend/taler-merchant-httpd_history.h +++ b/src/backend/taler-merchant-httpd_orders_get.h diff --git a/src/backend/taler-merchant-httpd_pay.h b/src/backend/taler-merchant-httpd_orders_order_abort.h index 726a27be..726a27be 100644 --- a/src/backend/taler-merchant-httpd_pay.h +++ b/src/backend/taler-merchant-httpd_orders_order_abort.h diff --git a/src/backend/taler-merchant-httpd_poll-payment.c b/src/backend/taler-merchant-httpd_orders_order_get.c index 6ca4fcce..6ca4fcce 100644 --- a/src/backend/taler-merchant-httpd_poll-payment.c +++ b/src/backend/taler-merchant-httpd_orders_order_get.c diff --git a/src/backend/taler-merchant-httpd_poll-payment.h b/src/backend/taler-merchant-httpd_orders_order_get.h index ac13c4a3..ac13c4a3 100644 --- a/src/backend/taler-merchant-httpd_poll-payment.h +++ b/src/backend/taler-merchant-httpd_orders_order_get.h diff --git a/src/backend/taler-merchant-httpd_refund_lookup.c b/src/backend/taler-merchant-httpd_orders_order_get2.c index e86e4e4b..e86e4e4b 100644 --- a/src/backend/taler-merchant-httpd_refund_lookup.c +++ b/src/backend/taler-merchant-httpd_orders_order_get2.c diff --git a/src/backend/taler-merchant-httpd_refund_lookup.h b/src/backend/taler-merchant-httpd_orders_order_get2.h index 24495daf..24495daf 100644 --- a/src/backend/taler-merchant-httpd_refund_lookup.h +++ b/src/backend/taler-merchant-httpd_orders_order_get2.h diff --git a/src/backend/taler-merchant-httpd_track-transaction.c b/src/backend/taler-merchant-httpd_orders_order_get3.c index 39f8ce9e..39f8ce9e 100644 --- a/src/backend/taler-merchant-httpd_track-transaction.c +++ b/src/backend/taler-merchant-httpd_orders_order_get3.c diff --git a/src/backend/taler-merchant-httpd_check-payment.c b/src/backend/taler-merchant-httpd_orders_order_get4.c index bb5384d1..bb5384d1 100644 --- a/src/backend/taler-merchant-httpd_check-payment.c +++ b/src/backend/taler-merchant-httpd_orders_order_get4.c diff --git a/src/backend/taler-merchant-httpd_proposal.c b/src/backend/taler-merchant-httpd_orders_order_get5.c index 47207131..47207131 100644 --- a/src/backend/taler-merchant-httpd_proposal.c +++ b/src/backend/taler-merchant-httpd_orders_order_get5.c diff --git a/src/backend/taler-merchant-httpd_proposal.h b/src/backend/taler-merchant-httpd_orders_order_get5.h index 677fee0e..677fee0e 100644 --- a/src/backend/taler-merchant-httpd_proposal.h +++ b/src/backend/taler-merchant-httpd_orders_order_get5.h diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_orders_order_pay.c index 7a1b7fd8..7a1b7fd8 100644 --- a/src/backend/taler-merchant-httpd_pay.c +++ b/src/backend/taler-merchant-httpd_orders_order_pay.c diff --git a/src/backend/taler-merchant-httpd_orders_order_pay.h b/src/backend/taler-merchant-httpd_orders_order_pay.h new file mode 100644 index 00000000..726a27be --- /dev/null +++ b/src/backend/taler-merchant-httpd_orders_order_pay.h @@ -0,0 +1,54 @@ +/* + This file is part of TALER + (C) 2014-2017 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/taler-merchant-httpd_pay.h + * @brief headers for /pay handler + * @author Marcello Stanisci + */ +#ifndef TALER_EXCHANGE_HTTPD_PAY_H +#define TALER_EXCHANGE_HTTPD_PAY_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume (void); + + +/** + * Manage a payment + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, never NULL + * @return MHD result code + */ +MHD_RESULT +MH_handler_pay (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi); + +#endif diff --git a/src/backend/taler-merchant-httpd_refund_increase.c b/src/backend/taler-merchant-httpd_orders_order_refund.c index 5324c619..5324c619 100644 --- a/src/backend/taler-merchant-httpd_refund_increase.c +++ b/src/backend/taler-merchant-httpd_orders_order_refund.c diff --git a/src/backend/taler-merchant-httpd_refund_increase.h b/src/backend/taler-merchant-httpd_orders_order_refund.h index ff178001..ff178001 100644 --- a/src/backend/taler-merchant-httpd_refund_increase.h +++ b/src/backend/taler-merchant-httpd_orders_order_refund.h diff --git a/src/backend/taler-merchant-httpd_order.h b/src/backend/taler-merchant-httpd_orders_post.h index cf43d1ba..cf43d1ba 100644 --- a/src/backend/taler-merchant-httpd_order.h +++ b/src/backend/taler-merchant-httpd_orders_post.h diff --git a/src/backend/taler-merchant-httpd_post-orders-order-abort.c b/src/backend/taler-merchant-httpd_post-orders-order-abort.c new file mode 100644 index 00000000..7a1b7fd8 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-orders-order-abort.c @@ -0,0 +1,2255 @@ +/* + This file is part of TALER + (C) 2014-2020 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 <http://www.gnu.org/licenses/> +*/ + +/** + * @file backend/taler-merchant-httpd_pay.c + * @brief handling of /pay requests + * @author Marcello Stanisci + * @author Christian Grothoff + * @author Florian Dold + */ +#include "platform.h" +#include <jansson.h> +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_exchange_service.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_refund.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define PAY_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 /pay handler. + */ +struct PayContext; + +/** + * Information kept during a /pay request for each coin. + */ +struct DepositConfirmation +{ + + /** + * Reference to the main PayContext + */ + struct PayContext *pc; + + /** + * Handle to the deposit operation we are performing for + * this coin, NULL after the operation is done. + */ + struct TALER_EXCHANGE_DepositHandle *dh; + + /** + * URL of the exchange that issued this coin. + */ + char *exchange_url; + + /** + * Denomination of this coin. + */ + struct TALER_DenominationPublicKey denom; + + /** + * Amount this coin contributes to the total purchase price. + * This amount includes the deposit fee. + */ + struct TALER_Amount amount_with_fee; + + /** + * 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; + + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Signature using the @e denom key over the @e coin_pub. + */ + struct TALER_DenominationSignature ub_sig; + + /** + * Signature of the coin's private key over the contract. + */ + struct TALER_CoinSpendSignatureP coin_sig; + + /** + * Offset of this coin into the `dc` array of all coins in the + * @e pc. + */ + unsigned int index; + + /** + * #GNUNET_YES if we found this coin in the database. + */ + int found_in_db; + + /** + * #GNUNET_YES if this coin was refunded. + */ + int refunded; + +}; + + +/** + * Information we keep for an individual call to the /pay handler. + */ +struct PayContext +{ + + /** + * This field MUST be first for handle_mhd_completion_callback() to work + * when it treats this struct as a `struct TM_HandlerContext`. + */ + struct TM_HandlerContext hc; + + /** + * Stored in a DLL. + */ + struct PayContext *next; + + /** + * Stored in a DLL. + */ + struct PayContext *prev; + + /** + * Array with @e coins_cnt coins we are despositing. + */ + struct DepositConfirmation *dc; + + /** + * MHD connection to return to + */ + struct MHD_Connection *connection; + + /** + * Instance of the payment's instance (in JSON format) + */ + struct MerchantInstance *mi; + + /** + * What wire method (of the @e mi) was selected by the wallet? + * Set in #parse_pay(). + */ + struct WireMethod *wm; + + /** + * Proposal data for the proposal that is being + * paid for in this context. + */ + json_t *contract_terms; + + /** + * 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; + + /** + * Handle to the exchange that we are doing the payment 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_FindOperation *fo; + + /** + * URL of the exchange used for the last @e fo. + */ + const char *current_exchange; + + /** + * 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. + */ + char *order_id; + + /** + * Fulfillment URL from @e contract_terms. + */ + char *fulfillment_url; + + /** + * Hashed proposal. + */ + struct GNUNET_HashCode h_contract_terms; + + /** + * "h_wire" from @e contract_terms. Used to identify + * the instance's wire transfer method. + */ + struct GNUNET_HashCode 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_Absolute wire_transfer_deadline; + + /** + * Timestamp from @e contract_terms. + */ + struct GNUNET_TIME_Absolute timestamp; + + /** + * Refund deadline from @e contract_terms. + */ + struct GNUNET_TIME_Absolute refund_deadline; + + /** + * Deadline for the customer to pay for this proposal. + */ + struct GNUNET_TIME_Absolute pay_deadline; + + /** + * 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; + + /** + * Number of coins this payment is made of. Length + * of the @e dc array. + */ + 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. + */ + 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). + */ + 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. + */ + int suspended; + + /** + * #GNUNET_YES if we already tried a forced /keys download. + */ + int tried_force_keys; + + /** + * Which operational mode is the /pay request made in? + */ + enum { PC_MODE_PAY, PC_MODE_ABORT_REFUND } mode; + +}; + + +/** + * Head of active pay context DLL. + */ +static struct PayContext *pc_head; + +/** + * Tail of active pay context DLL. + */ +static struct PayContext *pc_tail; + + +/** + * Abort all pending /deposit operations. + * + * @param pc pay context to abort + */ +static void +abort_deposit (struct PayContext *pc) +{ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Aborting pending /deposit operations\n"); + for (unsigned int i = 0; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dci = &pc->dc[i]; + + if (NULL != dci->dh) + { + TALER_EXCHANGE_deposit_cancel (dci->dh); + dci->dh = NULL; + } + } +} + + +/** + * Force all pay contexts to be resumed as we are about + * to shut down MHD. + */ +void +MH_force_pc_resume () +{ + for (struct PayContext *pc = pc_head; + NULL != pc; + pc = pc->next) + { + abort_deposit (pc); + 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 as exchange interaction is done (%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); + TMH_trigger_daemon (); /* 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)); +} + + +/** + * Generate a response that indicates payment success. + * + * @param pc payment context + */ +static void +generate_success_response (struct PayContext *pc) +{ + json_t *refunds; + struct GNUNET_CRYPTO_EddsaSignature sig; + + /* Check for applicable refunds */ + { + enum TALER_ErrorCode ec; + const char *errmsg; + + refunds = TM_get_refund_json (pc->mi, + &pc->h_contract_terms, + &ec, + &errmsg); + /* We would get an EMPTY array back on success if there + are no refunds, but not NULL. So NULL is always an error. */ + if (NULL == refunds) + { + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + ec, + errmsg); + return; + } + } + + /* Sign on our end (as the payment did go through, even if it may + have been refunded already) */ + { + struct PaymentResponsePS mr = { + .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK), + .purpose.size = htonl (sizeof (mr)), + .h_contract_terms = pc->h_contract_terms + }; + + GNUNET_CRYPTO_eddsa_sign (&pc->mi->privkey.eddsa_priv, + &mr, + &sig); + } + + /* Build the response */ + { + json_t *resp; + + resp = json_pack ("{s:O, s:o, s:o, s:o}", + "contract_terms", + pc->contract_terms, + "sig", + GNUNET_JSON_from_data_auto (&sig), + "h_contract_terms", + GNUNET_JSON_from_data (&pc->h_contract_terms, + sizeof (struct GNUNET_HashCode)), + "refund_permissions", + refunds); + if (NULL == resp) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not build final response"); + return; + } + resume_pay_with_response (pc, + MHD_HTTP_OK, + TALER_MHD_make_json (resp)); + json_decref (resp); + } +} + + +/** + * Custom cleanup routine for a `struct PayContext`. + * + * @param hc the `struct PayContext` to clean up. + */ +static void +pay_context_cleanup (struct TM_HandlerContext *hc) +{ + struct PayContext *pc = (struct PayContext *) hc; + + if (NULL != pc->timeout_task) + { + GNUNET_SCHEDULER_cancel (pc->timeout_task); + pc->timeout_task = NULL; + } + TALER_MHD_parse_post_cleanup_callback (pc->json_parse_context); + abort_deposit (pc); + for (unsigned int i = 0; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + if (NULL != dc->denom.rsa_public_key) + { + GNUNET_CRYPTO_rsa_public_key_free (dc->denom.rsa_public_key); + dc->denom.rsa_public_key = NULL; + } + if (NULL != dc->ub_sig.rsa_signature) + { + 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) + { + TMH_EXCHANGES_find_exchange_cancel (pc->fo); + pc->fo = NULL; + } + if (NULL != pc->response) + { + MHD_destroy_response (pc->response); + pc->response = NULL; + } + if (NULL != pc->contract_terms) + { + json_decref (pc->contract_terms); + pc->contract_terms = NULL; + } + GNUNET_free_non_null (pc->order_id); + GNUNET_free_non_null (pc->session_id); + GNUNET_free_non_null (pc->fulfillment_url); + GNUNET_CONTAINER_DLL_remove (pc_head, + pc_tail, + pc); + GNUNET_free (pc); +} + + +/** + * Check whether the amount paid is sufficient to cover + * the contract. + * + * @param pc payment context to check + * @return #GNUNET_OK if the payment is sufficient, #GNUNET_SYSERR if it is + * insufficient + */ +static int +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) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PAY_PAYMENT_INSUFFICIENT, + "insufficient funds (no coins!)"); + return GNUNET_SYSERR; + } + + acc_fee = pc->dc[0].deposit_fee; + total_wire_fee = pc->dc[0].wire_fee; + acc_amount = pc->dc[0].amount_with_fee; + + /** + * This loops calculates what are the deposit fee / total + * amount with fee / and wire fee, for all the coins. + */ + for (unsigned int i = 1; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + GNUNET_assert (GNUNET_YES == dc->found_in_db); + if ( (0 > + TALER_amount_add (&acc_fee, + &dc->deposit_fee, + &acc_fee)) || + (0 > + TALER_amount_add (&acc_amount, + &dc->amount_with_fee, + &acc_amount)) ) + { + GNUNET_break (0); + /* Overflow in these amounts? Very strange. */ + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts"); + } + if (1 == + TALER_amount_cmp (&dc->deposit_fee, + &dc->amount_with_fee)) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PAY_FEES_EXCEED_PAYMENT, + "Deposit fees exceed coin's contribution"); + return GNUNET_SYSERR; + } + + /* If exchange differs, add wire fee */ + { + int new_exchange = GNUNET_YES; + + for (unsigned int j = 0; j<i; j++) + if (0 == strcasecmp (dc->exchange_url, + pc->dc[j].exchange_url)) + { + new_exchange = GNUNET_NO; + break; + } + if (GNUNET_YES == new_exchange) + { + if (GNUNET_OK != + TALER_amount_cmp_currency (&total_wire_fee, + &dc->wire_fee)) + { + GNUNET_break_op (0); + resume_pay_with_error (pc, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_PAY_WIRE_FEE_CURRENCY_MISMATCH, + "exchange wire in different currency"); + return GNUNET_SYSERR; + } + 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_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED, + "could not add exchange wire fee to total"); + return GNUNET_SYSERR; + } + } + } + } + + 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)) + { + resume_pay_with_error (pc, + MHD_HTTP_PRECONDITION_FAILED, + TALER_EC_PAY_WIRE_FEE_CURRENCY_MISMATCH, + "exchange wire does not match our currency"); + return GNUNET_SYSERR; + } + + 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_get_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_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts"); + return GNUNET_SYSERR; + } + 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_PAY_AMOUNT_OVERFLOW, + "Overflow adding up amounts"); + return GNUNET_SYSERR; + } + } + 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_PAY_REFUNDS_EXCEED_PAYMENTS, + "refunded amount exceeds total payments"); + return GNUNET_SYSERR; + } + + if (-1 == TALER_amount_cmp (&final_amount, + &total_needed)) + { + /* acc_amount < total_needed */ + if (-1 < TALER_amount_cmp (&acc_amount, + &total_needed)) + { + resume_pay_with_error (pc, + MHD_HTTP_PAYMENT_REQUIRED, + TALER_EC_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_PAY_PAYMENT_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_PAY_PAYMENT_INSUFFICIENT, + "payment insufficient"); + + } + return GNUNET_SYSERR; + } + + + return GNUNET_OK; +} + + +/** + * 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 hr HTTP response code details + * @param exchange_sig signature from the exchange over the deposit confirmation + * @param sign_key which key did the exchange use to sign the @a proof + */ +static void +deposit_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_ExchangePublicKeyP *sign_key) +{ + 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 != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Deposit operation failed with HTTP code %u/%d\n", + hr->http_status, + (int) hr->ec); + /* Transaction failed; stop all other ongoing deposits */ + abort_deposit (pc); + + if (5 == hr->http_status / 100) + { + /* internal server error at exchange */ + resume_pay_with_response (pc, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_MHD_make_json_pack ( + "{s:s, s:I, s:I, s:I}", + "hint", + "exchange had an internal server error", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, + "exchange_code", + (json_int_t) hr->ec, + "exchange_http_status", + (json_int_t) hr->http_status)); + } + else if (NULL == hr->reply) + { + /* We can't do anything meaningful here, the exchange did something wrong */ + resume_pay_with_response (pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:s, s:I, s:I, s:I}", + "hint", + "exchange failed, response body not even in JSON", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, + "exchange_code", + (json_int_t) hr->ec, + "exchange_http_status", + (json_int_t) hr->http_status)); + } + else + { + /* Forward error, adding the "coin_pub" for which the + error was being generated */ + if (TALER_EC_DEPOSIT_INSUFFICIENT_FUNDS == hr->ec) + resume_pay_with_response ( + pc, + MHD_HTTP_CONFLICT, + TALER_MHD_make_json_pack ("{s:s, s:I, s:I, s:I, s:o, s:O}", + "hint", + "exchange failed on deposit of a coin", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, + "exchange_code", + (json_int_t) hr->ec, + "exchange_http_status", + (json_int_t) hr->http_status, + "coin_pub", + GNUNET_JSON_from_data_auto (&dc->coin_pub), + "exchange_reply", + hr->reply)); + else + resume_pay_with_response ( + pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ("{s:s, s:I, s:I, s:I, s:o, s:O}", + "hint", + "exchange failed on deposit of a coin", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED, + "exchange_code", + (json_int_t) hr->ec, + "exchange_http_status", + (json_int_t) hr->http_status, + "coin_pub", + GNUNET_JSON_from_data_auto (&dc->coin_pub), + "exchange_reply", + hr->reply)); + } + 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)); + /* NOTE: not run in any transaction block, simply as a + transaction by itself! */ + db->preflight (db->cls); + 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, + &dc->wire_fee, + sign_key, + hr->reply); + if (0 > qs) + { + /* Special report if retries insufficient */ + abort_deposit (pc); + 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); + /* 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--; + + 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 hr HTTP response details + * @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, + const struct TALER_EXCHANGE_HttpResponse *hr, + 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 (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", + "failed to obtain meta-data from exchange", + "code", + (json_int_t) TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", + (json_int_t) hr->http_status, + "exchange_code", + (json_int_t) hr->ec, + "exchange_reply", + hr->reply)); + return; + } + pc->mh = mh; + keys = TALER_EXCHANGE_get_keys (mh); + if (NULL == keys) + { + GNUNET_break (0); /* should not be possible if HTTP status is #MHD_HTTP_OK */ + resume_pay_with_error (pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_EC_PAY_EXCHANGE_KEYS_FAILURE, + "no keys"); + return; + } + + 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 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; + enum TALER_ErrorCode ec; + unsigned int hc; + + if (NULL != dc->dh) + continue; /* we were here before (can happen due to + tried_force_keys logic), don't go again */ + 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) + { + struct GNUNET_HashCode h_denom; + + if (! pc->tried_force_keys) + { + /* let's try *forcing* a re-download of /keys from the exchange. + Maybe the wallet has seen /keys that we missed. */ + pc->tried_force_keys = GNUNET_YES; + pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, + pc->wm->wire_method, + GNUNET_YES, + &process_pay_with_exchange, + pc); + if (NULL != pc->fo) + return; + } + /* Forcing failed or we already did it, give up */ + GNUNET_CRYPTO_rsa_public_key_hash (dc->denom.rsa_public_key, + &h_denom); + resume_pay_with_response ( + pc, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:s, s:I, s:o, s:o}", + "hint", "coin's denomination not found", + "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND, + "h_denom_pub", GNUNET_JSON_from_data_auto (&h_denom), + "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh))); + return; + } + if (GNUNET_OK != + TMH_AUDITORS_check_dk (mh, + denom_details, + exchange_trusted, + &hc, + &ec)) + { + resume_pay_with_response ( + pc, + hc, + TALER_MHD_make_json_pack ("{s:s, s:I, s:o}", + "hint", "denomination not accepted", + "code", (json_int_t) ec, + "h_denom_pub", GNUNET_JSON_from_data_auto ( + &denom_details->h_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->wm); + GNUNET_assert (NULL != pc->wm->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); + db->preflight (db->cls); + dc->dh = TALER_EXCHANGE_deposit (mh, + &dc->amount_with_fee, + pc->wire_transfer_deadline, + pc->wm->j_wire, + &pc->h_contract_terms, + &dc->coin_pub, + &dc->ub_sig, + &dc->denom, + pc->timestamp, + &pc->mi->pubkey, + pc->refund_deadline, + &dc->coin_sig, + &deposit_cb, + dc); + if (NULL == dc->dh) + { + /* Signature was invalid or some other constraint was not satisfied. If + the exchange was unavailable, we'd get that information in the + callback. */ + GNUNET_break_op (0); + resume_pay_with_response ( + pc, + MHD_HTTP_UNAUTHORIZED, + TALER_MHD_make_json_pack ( + "{s:s, s:I, s:i}", + "hint", "deposit signature invalid", + "code", (json_int_t) TALER_EC_PAY_COIN_SIGNATURE_INVALID, + "coin_idx", i)); + return; + } + if (TMH_force_audit) + TALER_EXCHANGE_deposit_force_dc (dc->dh); + 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) + { + db->preflight (db->cls); + pc->current_exchange = dc->exchange_url; + pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange, + pc->wm->wire_method, + GNUNET_NO, + &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_LOOKUP_FAILED, + "Failed to lookup exchange by URL"); + return; + } + return; + } + } + pc->current_exchange = NULL; + db->preflight (db->cls); + /* We are done with all the HTTP requests, go back and try + the 'big' database transaction! (It should work now!) */ + begin_transaction (pc); +} + + +/** + * 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_DEBUG, + "Resuming /pay with error after timeout\n"); + if (NULL != pc->fo) + { + TMH_EXCHANGES_find_exchange_cancel (pc->fo); + pc->fo = NULL; + } + resume_pay_with_error (pc, + MHD_HTTP_REQUEST_TIMEOUT, + TALER_EC_PAY_EXCHANGE_TIMEOUT, + "likely the exchange did not reply quickly enough"); +} + + +/** + * Function called with information about a coin that was deposited. + * + * @param cls closure + * @param h_contract_terms hashed proposal data + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @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 + * @param exchange_proof proof from exchange that coin was accepted + */ +static void +check_coin_paid (void *cls, + const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + 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, + const json_t *exchange_proof) +{ + struct PayContext *pc = cls; + + if (0 != GNUNET_memcmp (&pc->h_contract_terms, + h_contract_terms)) + { + GNUNET_break (0); + return; + } + 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 != GNUNET_memcmp (coin_pub, + &dc->coin_pub)) || + (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))); + if (0 > + 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 (0 > + 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; + dc->amount_with_fee = *amount_with_fee; + dc->found_in_db = GNUNET_YES; + pc->pending--; + } +} + + +/** + * Try to parse the pay request into the given pay context. + * Schedules an error response in the connection on failure. + * + * @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 #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 MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) +{ + json_t *coins; + const char *order_id; + const char *mode; + struct TALER_MerchantPublicKeyP merchant_pub; + enum GNUNET_GenericReturnValue res; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("mode", + &mode), + 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; + + res = TALER_MHD_parse_json_data (connection, + root, + spec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); + return res; + } + + if (0 != GNUNET_memcmp (&merchant_pub, + &pc->mi->pubkey)) + { + GNUNET_JSON_parse_free (spec); + TALER_LOG_INFO ( + "Unknown merchant public key included in payment (usually wrong instance chosen)\n"); + return + (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PAY_WRONG_INSTANCE, + "merchant_pub in contract does not match this instance")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + { + const char *session_id; + + session_id = json_string_value (json_object_get (root, + "session_id")); + if (NULL != session_id) + pc->session_id = GNUNET_strdup (session_id); + } + GNUNET_assert (NULL == pc->order_id); + pc->order_id = GNUNET_strdup (order_id); + GNUNET_assert (NULL == pc->contract_terms); + qs = db->find_contract_terms (db->cls, + &pc->contract_terms, + order_id, + &merchant_pub); + if (0 > qs) + { + GNUNET_JSON_parse_free (spec); + /* 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 (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_DB_FETCH_PAY_ERROR, + "Failed to obtain contract terms from DB")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_JSON_parse_free (spec); + return + (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_PAY_PROPOSAL_NOT_FOUND, + "Proposal not found")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_JSON_hash (pc->contract_terms, + &pc->h_contract_terms)) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return + (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash proposal")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling /pay for order `%s' with contract hash `%s'\n", + order_id, + GNUNET_h2s (&pc->h_contract_terms)); + + if (NULL == json_object_get (pc->contract_terms, + "merchant")) + { + /* invalid contract */ + GNUNET_JSON_parse_free (spec); + return + (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_MERCHANT_FIELD_MISSING, + "No merchant field in proposal")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (0 != strcasecmp ("abort-refund", + mode)) + pc->mode = PC_MODE_PAY; + else + pc->mode = PC_MODE_ABORT_REFUND; + { + const char *fulfillment_url; + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_absolute_time ("refund_deadline", + &pc->refund_deadline), + GNUNET_JSON_spec_absolute_time ("pay_deadline", + &pc->pay_deadline), + GNUNET_JSON_spec_absolute_time ("wire_transfer_deadline", + &pc->wire_transfer_deadline), + GNUNET_JSON_spec_absolute_time ("timestamp", + &pc->timestamp), + TALER_JSON_spec_amount ("max_fee", + &pc->max_fee), + TALER_JSON_spec_amount ("amount", + &pc->amount), + GNUNET_JSON_spec_string ("fulfillment_url", + &fulfillment_url), + GNUNET_JSON_spec_fixed_auto ("h_wire", + &pc->h_wire), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + pc->contract_terms, + espec); + if (GNUNET_YES != res) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return res; + } + + pc->fulfillment_url = GNUNET_strdup (fulfillment_url); + if (pc->wire_transfer_deadline.abs_value_us < + pc->refund_deadline.abs_value_us) + { + /* This should already have been checked when creating the + order! */ + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE, + "refund deadline after wire transfer deadline"); + } + + if (pc->pay_deadline.abs_value_us < + GNUNET_TIME_absolute_get ().abs_value_us) + { + /* too late */ + GNUNET_JSON_parse_free (spec); + return + (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_GONE, + TALER_EC_PAY_OFFER_EXPIRED, + "The payment deadline has past and the offer is no longer valid")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + + } + + /* find wire method */ + { + struct WireMethod *wm; + + wm = pc->mi->wm_head; + while (0 != GNUNET_memcmp (&pc->h_wire, + &wm->h_wire)) + wm = wm->next; + if (NULL == wm) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_PAY_WIRE_HASH_UNKNOWN, + "Did not find matching wire details"); + } + pc->wm = wm; + } + + /* parse optional details */ + if (NULL != json_object_get (pc->contract_terms, + "max_wire_fee")) + { + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount ("max_wire_fee", + &pc->max_wire_fee), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + pc->contract_terms, + espec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); /* invalid input, fail */ + GNUNET_JSON_parse_free (spec); + return res; + } + } + else + { + /* default is we cover no fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (pc->max_fee.currency, + &pc->max_wire_fee)); + } + + if (NULL != json_object_get (pc->contract_terms, + "wire_fee_amortization")) + { + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_uint32 ("wire_fee_amortization", + &pc->wire_fee_amortization), + GNUNET_JSON_spec_end () + }; + + res = TALER_MHD_parse_json_data (connection, + pc->contract_terms, + espec); + if ( (GNUNET_YES != res) || + (0 == pc->wire_fee_amortization) ) + { + GNUNET_break_op (0); /* invalid input, use default */ + /* default is no amortization */ + pc->wire_fee_amortization = 1; + } + } + else + { + pc->wire_fee_amortization = 1; + } + + pc->coins_cnt = json_array_size (coins); + if (0 == pc->coins_cnt) + { + GNUNET_JSON_parse_free (spec); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PAY_COINS_ARRAY_EMPTY, + "coins"); + } + /* 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[] = { + 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 () + }; + + res = TALER_MHD_parse_json_data (connection, + coin, + ispec); + if (GNUNET_YES != res) + { + GNUNET_JSON_parse_free (spec); + GNUNET_break_op (0); + return res; + } + dc->exchange_url = GNUNET_strdup (exchange_url); + dc->index = coins_index; + dc->pc = pc; + } + } + pc->pending = pc->coins_cnt; + GNUNET_JSON_parse_free (spec); + return GNUNET_OK; +} + + +/** + * 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 exchange_url URL of the exchange that issued @a coin_pub + * @param rtransaction_id identificator of the refund + * @param reason human-readable explanation of the refund + * @param refund_amount refund amount which is being taken from @a coin_pub + * @param refund_fee cost of this refund operation + */ +static void +check_coin_refunded (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + uint64_t rtransaction_id, + const char *reason, + const struct TALER_Amount *refund_amount, + const struct TALER_Amount *refund_fee) +{ + struct PayContext *pc = cls; + + (void) exchange_url; + for (unsigned int i = 0; i<pc->coins_cnt; i++) + { + struct DepositConfirmation *dc = &pc->dc[i]; + + /* Get matching coin from results*/ + if (0 == GNUNET_memcmp (coin_pub, + &dc->coin_pub)) + { + dc->refunded = GNUNET_YES; + GNUNET_assert (0 <= + TALER_amount_add (&pc->total_refunded, + &pc->total_refunded, + refund_amount)); + } + } +} + + +/** + * 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; + + /* 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_PAY_DB_STORE_TRANSACTION_ERROR, + "Soft merchant database error: retry counter exceeded"); + return; + } + GNUNET_assert (GNUNET_YES == pc->suspended); + + /* Init. some price accumulators. */ + 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)); + + /* First, try to see if we have all we need already done */ + db->preflight (db->cls); + if (GNUNET_OK != + db->start (db->cls, + "run pay")) + { + 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 begin transaction)"); + return; + } + + /* Check if some of these coins already succeeded for _this_ contract. */ + qs = db->find_payments (db->cls, + &pc->h_contract_terms, + &pc->mi->pubkey, + &check_coin_paid, + 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; + } + + /* 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 checking for refunds"); + return; + } + + /* All the coins known to the database have + * been processed, now delve into specific case + * (pay vs. abort) */ + + if (PC_MODE_ABORT_REFUND == pc->mode) + { + json_t *terms; + + /* The wallet is going for a refund, + (on aborted operation)! */ + + /* check payment was indeed incomplete */ + qs = db->find_paid_contract_terms_from_hash (db->cls, + &terms, + &pc->h_contract_terms, + &pc->mi->pubkey); + 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_STORE_PAY_ERROR, + "Merchant database error"); + return; + } + if (0 < qs) + { + /* Payment had been complete! */ + json_decref (terms); + db->rollback (db->cls); + resume_pay_with_error (pc, + MHD_HTTP_FORBIDDEN, + TALER_EC_PAY_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE, + "Payment complete, refusing to abort"); + return; + } + + /* Store refund in DB */ + qs = db->increase_refund_for_contract_NT (db->cls, + &pc->h_contract_terms, + &pc->mi->pubkey, + &pc->total_paid, + /* justification */ + "incomplete payment aborted"); + 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_STORE_PAY_ERROR, + "Merchant database error storing abort-refund"); + return; + } + qs = db->commit (db->cls); + if (0 > qs) + { + db->rollback (db->cls); + 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_PAY_ERROR, + "Merchant database error: could not commit"); + return; + } + /* At this point, the refund got correctly committed + * into the database. */ + { + json_t *refunds; + + refunds = json_array (); + if (NULL == refunds) + { + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not create JSON array"); + return; + } + for (unsigned int i = 0; i<pc->coins_cnt; i++) + { + struct TALER_MerchantSignatureP msig; + struct TALER_RefundRequestPS rr = { + .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND), + .purpose.size = htonl (sizeof (rr)), + .h_contract_terms = pc->h_contract_terms, + .coin_pub = pc->dc[i].coin_pub, + .merchant = pc->mi->pubkey, + .rtransaction_id = GNUNET_htonll (0) + }; + + if (GNUNET_YES != pc->dc[i].found_in_db) + continue; /* Skip coins not found in DB. */ + TALER_amount_hton (&rr.refund_amount, + &pc->dc[i].amount_with_fee); + TALER_amount_hton (&rr.refund_fee, + &pc->dc[i].refund_fee); + + GNUNET_CRYPTO_eddsa_sign (&pc->mi->privkey.eddsa_priv, + &rr, + &msig.eddsa_sig); + /* Pack refund for i-th coin. */ + if (0 != + json_array_append_new ( + refunds, + json_pack ("{s:I, s:o, s:o s:o s:o}", + "rtransaction_id", + (json_int_t) 0, + "coin_pub", + GNUNET_JSON_from_data_auto (&rr.coin_pub), + "merchant_sig", + GNUNET_JSON_from_data_auto (&msig), + "refund_amount", + TALER_JSON_from_amount_nbo (&rr.refund_amount), + "refund_fee", + TALER_JSON_from_amount_nbo (&rr.refund_fee)))) + { + json_decref (refunds); + GNUNET_break (0); + resume_pay_with_error (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_JSON_ALLOCATION_FAILURE, + "could not create JSON array"); + return; + } + } + + /* Resume and send back the response. */ + resume_pay_with_response ( + pc, + MHD_HTTP_OK, + TALER_MHD_make_json_pack ( + "{s:o, s:o, s:o}", + /* Refunds pack. */ + "refund_permissions", refunds, + "merchant_pub", + GNUNET_JSON_from_data_auto (&pc->mi->pubkey), + "h_contract_terms", + GNUNET_JSON_from_data_auto (&pc->h_contract_terms))); + } + return; + } /* End of PC_MODE_ABORT_REFUND */ + + /* Default PC_MODE_PAY mode */ + + /* Final termination case: all coins already known, just + generate ultimate outcome. */ + if (0 == pc->pending) + { + if (GNUNET_OK != check_payment_sufficient (pc)) + { + db->rollback (db->cls); + return; + } + /* Payment succeeded, save in database */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Contract `%s' was fully paid\n", + GNUNET_h2s (&pc->h_contract_terms)); + qs = db->mark_proposal_paid (db->cls, + &pc->h_contract_terms, + &pc->mi->pubkey); + if (qs < 0) + { + db->rollback (db->cls); + 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; + } + + if ( (NULL != pc->session_id) && + (NULL != pc->fulfillment_url) ) + { + qs = db->insert_session_info (db->cls, + pc->session_id, + pc->fulfillment_url, + pc->order_id, + &pc->mi->pubkey); + } + + /* Now commit! */ + if (0 <= qs) + qs = db->commit (db->cls); + else + db->rollback (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 commit to mark proposal as 'paid'"); + return; + } + TMH_long_poll_resume (pc->order_id, + &pc->mi->pubkey, + NULL); + generate_success_response (pc); + return; + } + + + /* 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); +} + + +/** + * 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 MHD_RESULT +handler_pay_json (struct MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) +{ + { + enum GNUNET_GenericReturnValue ret; + + ret = parse_pay (connection, + root, + 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"); + pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, + &handle_pay_timeout, + pc); + begin_transaction (pc); + return MHD_YES; +} + + +/** + * Process a payment for a proposal. Takes data from the given MHD + * connection. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure + * (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a + * upload_data + * @param mi merchant backend instance, never NULL + * @return MHD result code + */ +MHD_RESULT +MH_handler_pay (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct PayContext *pc; + enum GNUNET_GenericReturnValue res; + MHD_RESULT ret; + json_t *root; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "In handler for /pay.\n"); + if (NULL == *connection_cls) + { + pc = GNUNET_new (struct PayContext); + GNUNET_CONTAINER_DLL_insert (pc_head, + pc_tail, + pc); + pc->hc.cc = &pay_context_cleanup; + pc->connection = connection; + *connection_cls = pc; + pc->mi = mi; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "/pay: picked instance %s\n", + mi->id); + } + else + { + /* not the first call, recover state */ + pc = *connection_cls; + } + if (GNUNET_SYSERR == pc->suspended) + return MHD_NO; /* during shutdown, we don't generate any more replies */ + 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 */ + } + res = MHD_queue_response (connection, + pc->response_code, + pc->response); + MHD_destroy_response (pc->response); + pc->response = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /pay (%s).\n", + (unsigned int) pc->response_code, + res ? "OK" : "FAILED"); + return res; + } + + res = TALER_MHD_parse_post_json (connection, + &pc->json_parse_context, + upload_data, + upload_data_size, + &root); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_JSON_INVALID, + "could not parse JSON"); + } + if ( (GNUNET_NO == res) || + (NULL == root) ) + return MHD_YES; /* the POST's body has to be further fetched */ + + ret = handler_pay_json (connection, + root, + pc); + json_decref (root); + return ret; +} + + +/* end of taler-merchant-httpd_pay.c */ diff --git a/src/backend/taler-merchant-httpd_order.c b/src/backend/taler-merchant-httpd_post-orders.c index 317e451b..317e451b 100644 --- a/src/backend/taler-merchant-httpd_order.c +++ b/src/backend/taler-merchant-httpd_post-orders.c diff --git a/src/backend/taler-merchant-httpd_tip-pickup.c b/src/backend/taler-merchant-httpd_post-tips-tip-pickup.c index 51dd8121..51dd8121 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup.c +++ b/src/backend/taler-merchant-httpd_post-tips-tip-pickup.c diff --git a/src/backend/taler-merchant-httpd_tip-authorize.c b/src/backend/taler-merchant-httpd_post-tips.c index 569cf0ab..569cf0ab 100644 --- a/src/backend/taler-merchant-httpd_tip-authorize.c +++ b/src/backend/taler-merchant-httpd_post-tips.c diff --git a/src/backend/taler-merchant-httpd_post-transfers.c b/src/backend/taler-merchant-httpd_post-transfers.c new file mode 100644 index 00000000..7f55c917 --- /dev/null +++ b/src/backend/taler-merchant-httpd_post-transfers.c @@ -0,0 +1,1089 @@ +/* + This file is part of TALER + (C) 2014-2020 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 <http://www.gnu.org/licenses/> +*/ +/** + * @file backend/taler-merchant-httpd_track-transfer.c + * @brief implement API for tracking transfers and wire transfers + * @author Marcello Stanisci + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <taler/taler_signatures.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd.h" +#include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_auditors.h" +#include "taler-merchant-httpd_exchanges.h" +#include "taler-merchant-httpd_track-transfer.h" + + +/** + * How long to wait before giving up processing with the exchange? + */ +#define TRACK_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, \ + 30)) + +/** + * How often do we retry the simple INSERT database transaction? + */ +#define MAX_RETRIES 3 + +/** + * Context used for handing /track/transfer requests. + */ +struct TrackTransferContext +{ + + /** + * This MUST be first! + */ + struct TM_HandlerContext hc; + + /** + * Handle to the exchange. + */ + struct TALER_EXCHANGE_Handle *eh; + + /** + * Handle for the /wire/transfers request. + */ + struct TALER_EXCHANGE_TransfersGetHandle *wdh; + + /** + * For which merchant instance is this tracking request? + */ + struct MerchantInstance *mi; + + /** + * HTTP connection we are handling. + */ + struct MHD_Connection *connection; + + /** + * Response to return upon resume. + */ + struct MHD_Response *response; + + /** + * Handle for operation to lookup /keys (and auditors) from + * the exchange used for this transaction; NULL if no operation is + * pending. + */ + struct TMH_EXCHANGES_FindOperation *fo; + + /** + * Task run on timeout. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * URL of the exchange. + */ + char *url; + + /** + * Wire method used for the transfer. + */ + char *wire_method; + + /** + * Pointer to the detail that we are currently + * checking in #check_transfer(). + */ + const struct TALER_TrackTransferDetails *current_detail; + + /** + * Argument for the /wire/transfers request. + */ + struct TALER_WireTransferIdentifierRawP wtid; + + /** + * Full original response we are currently processing. + */ + const json_t *original_response; + + /** + * Modified response to return to the frontend. + */ + json_t *deposits_response; + + /** + * Which transaction detail are we currently looking at? + */ + unsigned int current_offset; + + /** + * Response code to return. + */ + unsigned int response_code; + + /** + * #GNUNET_NO if we did not find a matching coin. + * #GNUNET_SYSERR if we found a matching coin, but the amounts do not match. + * #GNUNET_OK if we did find a matching coin. + */ + int check_transfer_result; +}; + + +/** + * Represents an entry in the table used to sum up + * individual deposits for each h_contract_terms. + */ +struct Entry +{ + + /** + * Sum accumulator for deposited value. + */ + struct TALER_Amount deposit_value; + + /** + * Sum accumulator for deposit fee. + */ + struct TALER_Amount deposit_fee; + +}; + + +/** + * Free the @a rctx. + * + * @param rctx data to free + */ +static void +free_transfer_track_context (struct TrackTransferContext *rctx) +{ + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + if (NULL != rctx->wdh) + { + TALER_EXCHANGE_transfers_get_cancel (rctx->wdh); + rctx->wdh = NULL; + } + if (NULL != rctx->url) + { + GNUNET_free (rctx->url); + rctx->url = NULL; + } + if (NULL != rctx->wire_method) + { + GNUNET_free (rctx->wire_method); + rctx->wire_method = NULL; + } + GNUNET_free (rctx); +} + + +/** + * Callback that frees all the elements in the hashmap + * + * @param cls closure, NULL + * @param key current key + * @param value a `struct Entry` + * @return #GNUNET_YES if the iteration should continue, + * #GNUNET_NO otherwise. + */ +static int +hashmap_free (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TALER_Entry *entry = value; + + (void) cls; + (void) key; + GNUNET_free (entry); + return GNUNET_YES; +} + + +/** + * Builds JSON response containing the summed-up amounts + * from individual deposits. + * + * @param cls closure + * @param key map's current key + * @param map's current value + * @return #GNUNET_YES if iteration is to be continued, + * #GNUNET_NO otherwise. + */ +static int +build_deposits_response (void *cls, + const struct GNUNET_HashCode *key, + void *value) +{ + struct TrackTransferContext *rctx = cls; + struct Entry *entry = value; + json_t *element; + json_t *contract_terms; + json_t *order_id; + + db->preflight (db->cls); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + db->find_contract_terms_from_hash (db->cls, + &contract_terms, + key, + &rctx->mi->pubkey)) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + + order_id = json_object_get (contract_terms, + "order_id"); + if (NULL == order_id) + { + GNUNET_break_op (0); + json_decref (contract_terms); + return GNUNET_NO; + } + element = json_pack ("{s:O, s:o, s:o}", + "order_id", order_id, + "deposit_value", TALER_JSON_from_amount ( + &entry->deposit_value), + "deposit_fee", TALER_JSON_from_amount ( + &entry->deposit_fee)); + json_decref (contract_terms); + if (NULL == element) + { + GNUNET_break_op (0); + return GNUNET_NO; + } + GNUNET_break (0 == + json_array_append_new (rctx->deposits_response, + element)); + return GNUNET_YES; +} + + +/** + * Transform /track/transfer result as gotten from the exchange + * and transforms it in a format liked by the backoffice Web interface. + * + * @param result response from exchange's /track/transfer + * @result pointer to new JSON, or NULL upon errors. + */ +static json_t * +transform_response (const json_t *result, + struct TrackTransferContext *rctx) +{ + json_t *deposits; + json_t *value; + json_t *result_mod = NULL; + size_t index; + const char *key; + struct GNUNET_HashCode h_key; + struct GNUNET_CONTAINER_MultiHashMap *map; + struct TALER_Amount iter_value; + struct TALER_Amount iter_fee; + struct Entry *current_entry; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount ("deposit_value", &iter_value), + TALER_JSON_spec_amount ("deposit_fee", &iter_fee), + GNUNET_JSON_spec_string ("h_contract_terms", &key), + GNUNET_JSON_spec_end () + }; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Transforming /track/transfer response.\n"); + map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO); + deposits = json_object_get (result, + "deposits"); + + json_array_foreach (deposits, index, value) + { + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + NULL, + NULL)) + { + GNUNET_break_op (0); + return NULL; + } + GNUNET_CRYPTO_hash_from_string (key, + &h_key); + + if (NULL != (current_entry = + GNUNET_CONTAINER_multihashmap_get (map, + &h_key))) + { + /* The map already knows this h_contract_terms*/ + if ( (0 > + TALER_amount_add (¤t_entry->deposit_value, + ¤t_entry->deposit_value, + &iter_value)) || + (0 > + TALER_amount_add (¤t_entry->deposit_fee, + ¤t_entry->deposit_fee, + &iter_fee)) ) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + else + { + /* First time in the map for this h_contract_terms*/ + current_entry = GNUNET_new (struct Entry); + current_entry->deposit_value = iter_value; + current_entry->deposit_fee = iter_fee; + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_put (map, + &h_key, + current_entry, + GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY)) + { + GNUNET_JSON_parse_free (spec); + goto cleanup; + } + } + GNUNET_JSON_parse_free (spec); + } + rctx->deposits_response = json_array (); + + if (GNUNET_SYSERR == + GNUNET_CONTAINER_multihashmap_iterate (map, + &build_deposits_response, + rctx)) + goto cleanup; + + result_mod = json_copy ((struct json_t *) result); + json_object_del (result_mod, + "deposits"); + json_object_set_new (result_mod, + "deposits_sums", + rctx->deposits_response); + rctx->deposits_response = NULL; +cleanup: + GNUNET_CONTAINER_multihashmap_iterate (map, + &hashmap_free, + NULL); + GNUNET_CONTAINER_multihashmap_destroy (map); + return result_mod; +} + + +/** + * Resume the given /track/transfer operation and send the given response. + * Stores the response in the @a rctx and signals MHD to resume + * the connection. Also ensures MHD runs immediately. + * + * @param rctx transfer tracking context + * @param response_code response code to use + * @param response response data to send back + */ +static void +resume_track_transfer_with_response (struct TrackTransferContext *rctx, + unsigned int response_code, + struct MHD_Response *response) +{ + rctx->response_code = response_code; + rctx->response = response; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer handling as exchange interaction is done (%u)\n", + response_code); + if (NULL != rctx->timeout_task) + { + GNUNET_SCHEDULER_cancel (rctx->timeout_task); + rctx->timeout_task = NULL; + } + MHD_resume_connection (rctx->connection); + TMH_trigger_daemon (); /* we resumed, kick MHD */ +} + + +/** + * Custom cleanup routine for a `struct TrackTransferContext`. + * + * @param hc the `struct TrackTransferContext` to clean up. + */ +static void +track_transfer_cleanup (struct TM_HandlerContext *hc) +{ + struct TrackTransferContext *rctx = (struct TrackTransferContext *) hc; + + free_transfer_track_context (rctx); +} + + +/** + * This function checks that the information about the coin which + * was paid back by _this_ wire transfer matches what _we_ (the merchant) + * knew about this coin. + * + * @param cls closure with our `struct TrackTransferContext *` + * @param transaction_id of the contract + * @param coin_pub public key of the coin + * @param exchange_url URL of the exchange that issued @a coin_pub + * @param amount_with_fee amount the exchange will transfer 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 exchange_proof proof from exchange that coin was accepted + */ +static void +check_transfer (void *cls, + const struct GNUNET_HashCode *h_contract_terms, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const char *exchange_url, + 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, + const json_t *exchange_proof) +{ + struct TrackTransferContext *rctx = cls; + const struct TALER_TrackTransferDetails *ttd = rctx->current_detail; + + if (GNUNET_SYSERR == rctx->check_transfer_result) + return; /* already had a serious issue; odd that we're called more than once as well... */ + if ( (0 != TALER_amount_cmp (amount_with_fee, + &ttd->coin_value)) || + (0 != TALER_amount_cmp (deposit_fee, + &ttd->coin_fee)) ) + { + /* Disagreement between the exchange and us about how much this + coin is worth! */ + GNUNET_break_op (0); + rctx->check_transfer_result = GNUNET_SYSERR; + /* Build the `TrackTransferConflictDetails` */ + rctx->response + = TALER_MHD_make_json_pack ( + "{s:I, s:s, s:o, s:I, s:o, s:o, s:s, s:o, s:o}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_CONFLICTING_REPORTS, + "hint", "disagreement about deposit valuation", + "exchange_deposit_proof", exchange_proof, + "conflict_offset", (json_int_t) rctx->current_offset, + "exchange_transfer_proof", rctx->original_response, + "coin_pub", GNUNET_JSON_from_data_auto (coin_pub), + "h_contract_terms", GNUNET_JSON_from_data_auto ( + &ttd->h_contract_terms), + "amount_with_fee", TALER_JSON_from_amount (amount_with_fee), + "deposit_fee", TALER_JSON_from_amount (deposit_fee)); + return; + } + rctx->check_transfer_result = GNUNET_OK; +} + + +/** + * Check that the given @a wire_fee is what the + * @a exchange_pub should charge at the @a execution_time. + * If the fee is correct (according to our database), + * return #GNUNET_OK. If we do not have the fee structure + * in our DB, we just accept it and return #GNUNET_NO; + * if we have proof that the fee is bogus, we respond with + * the proof to the client and return #GNUNET_SYSERR. + * + * @param rctx context of the transfer to respond to + * @param json response from the exchange + * @param execution_time time of the wire transfer + * @param wire_fee fee claimed by the exchange + * @return #GNUNET_SYSERR if we returned hard proof of + * missbehavior from the exchange to the client + */ +static int +check_wire_fee (struct TrackTransferContext *rctx, + const json_t *json, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *wire_fee) +{ + const struct TALER_MasterPublicKeyP *master_pub; + struct GNUNET_HashCode h_wire_method; + struct TALER_Amount expected_fee; + struct TALER_Amount closing_fee; + struct TALER_MasterSignatureP master_sig; + struct GNUNET_TIME_Absolute start_date; + struct GNUNET_TIME_Absolute end_date; + enum GNUNET_DB_QueryStatus qs; + const struct TALER_EXCHANGE_Keys *keys; + + keys = TALER_EXCHANGE_get_keys (rctx->eh); + if (NULL == keys) + { + GNUNET_break (0); + return GNUNET_NO; + } + master_pub = &keys->master_pub; + GNUNET_CRYPTO_hash (rctx->wire_method, + strlen (rctx->wire_method) + 1, + &h_wire_method); + db->preflight (db->cls); + qs = db->lookup_wire_fee (db->cls, + master_pub, + &h_wire_method, + execution_time, + &expected_fee, + &closing_fee, + &start_date, + &end_date, + &master_sig); + if (0 >= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find wire fee for `%s' and method `%s' at %s in DB, accepting blindly that the fee is %s\n", + TALER_B2S (master_pub), + rctx->wire_method, + GNUNET_STRINGS_absolute_time_to_string (execution_time), + TALER_amount2s (wire_fee)); + return GNUNET_NO; + } + if (0 <= TALER_amount_cmp (&expected_fee, + wire_fee)) + return GNUNET_OK; /* expected_fee >= wire_fee */ + + /* Wire fee check failed, export proof to client */ + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:o, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_JSON_BAD_WIRE_FEE, + "wire_fee", TALER_JSON_from_amount (wire_fee), + "execution_time", GNUNET_JSON_from_time_abs (execution_time), + "expected_wire_fee", TALER_JSON_from_amount (&expected_fee), + "expected_closing_fee", TALER_JSON_from_amount (&closing_fee), + "start_date", GNUNET_JSON_from_time_abs (start_date), + "end_date", GNUNET_JSON_from_time_abs (end_date), + "master_sig", GNUNET_JSON_from_data_auto (&master_sig), + "master_pub", GNUNET_JSON_from_data_auto (master_pub), + "json", json)); + return GNUNET_SYSERR; +} + + +/** + * Function called with detailed wire transfer data, including all + * of the coin transactions that were combined into the wire transfer. + * + * @param cls closure + * @param hr HTTP response details + * @param exchange_pub public key of the exchange used to sign @a json + * @param h_wire hash of the wire transfer address the transfer went to, or NULL on error + * @param execution_time time when the exchange claims to have performed the wire transfer + * @param total_amount total amount of the wire transfer, or NULL if the exchange could + * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange + * @param details_length length of the @a details array + * @param details array with details about the combined transactions + */ +static void +wire_transfer_cb (void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct GNUNET_HashCode *h_wire, + struct GNUNET_TIME_Absolute execution_time, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + unsigned int details_length, + const struct TALER_TrackTransferDetails *details) +{ + struct TrackTransferContext *rctx = cls; + json_t *jresponse; + enum GNUNET_DB_QueryStatus qs; + + rctx->wdh = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Got response code %u from exchange for /track/transfer\n", + hr->http_status); + if (MHD_HTTP_OK != hr->http_status) + { + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + "{s:I, s:I, s:I, s:O}", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_ERROR, + "exchange_code", (json_int_t) hr->ec, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_reply", hr->reply)); + return; + } + for (unsigned int i = 0; i<MAX_RETRIES; i++) + { + db->preflight (db->cls); + qs = db->store_transfer_to_proof (db->cls, + rctx->url, + &rctx->wtid, + execution_time, + exchange_pub, + hr->reply); + 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); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_TRANSFER_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + rctx->original_response = hr->reply; + + if (GNUNET_SYSERR == + check_wire_fee (rctx, + hr->reply, + execution_time, + wire_fee)) + return; + + /* Now we want to double-check that any (Taler coin) deposit + * which is accounted into _this_ wire transfer, does exist + * into _our_ database. This is the rationale: if the + * exchange paid us for it, we must have received it _beforehands_! + * + * details_length is how many (Taler coin) deposits have been + * aggregated into _this_ wire transfer. + */// + for (unsigned int i = 0; i<details_length; i++) + { + rctx->current_offset = i; + rctx->current_detail = &details[i]; + /* Set the coin as "never seen" before. */ + rctx->check_transfer_result = GNUNET_NO; + db->preflight (db->cls); + qs = db->find_payments_by_hash_and_coin (db->cls, + &details[i].h_contract_terms, + &rctx->mi->pubkey, + &details[i].coin_pub, + &check_transfer, + rctx); + 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 as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "details", + "failed to obtain deposit data from local database")); + return; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + /* The exchange says we made this deposit, but WE do not + recall making it (corrupted / unreliable database?)! + Well, let's say thanks and accept the money! */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to find payment data in DB\n"); + rctx->check_transfer_result = GNUNET_OK; + } + if (GNUNET_NO == rctx->check_transfer_result) + { + /* Internal error: how can we have called #check_transfer() + but still have no result? */ + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s, s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_INTERNAL_LOGIC_ERROR, + "details", "internal logic error", + "line", (json_int_t) __LINE__, + "file", __FILE__)); + return; + } + if (GNUNET_SYSERR == rctx->check_transfer_result) + { + /* #check_transfer() failed, report conflict! */ + GNUNET_break_op (0); + GNUNET_assert (NULL != rctx->response); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_FAILED_DEPENDENCY, + rctx->response); + rctx->response = NULL; + return; + } + /* Response is consistent with the /deposit we made, + remember it for future reference */ + for (unsigned int r = 0; r<MAX_RETRIES; r++) + { + db->preflight (db->cls); + qs = db->store_coin_to_transfer (db->cls, + &details[i].h_contract_terms, + &details[i].coin_pub, + &rctx->wtid); + 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); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_DB_STORE_COIN_ERROR, + "details", + "failed to store response from exchange to local database")); + return; + } + } + rctx->original_response = NULL; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "About to call tracks transformator.\n"); + + if (NULL == (jresponse = + transform_response (hr->reply, + rctx))) + { + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate the response.")); + return; + } + + resume_track_transfer_with_response (rctx, + MHD_HTTP_OK, + TALER_MHD_make_json (jresponse)); + json_decref (jresponse); +} + + +/** + * Function called with the result of our exchange lookup. + * + * @param cls the `struct TrackTransferContext` + * @param hr HTTP response details + * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) + * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config + */ +static void +process_track_transfer_with_exchange (void *cls, + const struct + TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, + int exchange_trusted) +{ + struct TrackTransferContext *rctx = cls; + + rctx->fo = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + /* The request failed somehow */ + GNUNET_break_op (0); + resume_track_transfer_with_response ( + rctx, + MHD_HTTP_FAILED_DEPENDENCY, + TALER_MHD_make_json_pack ( + (NULL != hr->reply) + ? "{s:s, s:I, s:I, s:I, s:O}" + : "{s:s, s:I, s:I, s:I}", + "hint", "failed to obtain meta-data from exchange", + "code", (json_int_t) TALER_EC_TRACK_TRANSFER_EXCHANGE_KEYS_FAILURE, + "exchange_http_status", (json_int_t) hr->http_status, + "exchange_code", (json_int_t) hr->ec, + "exchange_reply", hr->reply)); + return; + } + rctx->eh = eh; + rctx->wdh = TALER_EXCHANGE_transfers_get (eh, + &rctx->wtid, + &wire_transfer_cb, + rctx); + if (NULL == rctx->wdh) + { + GNUNET_break (0); + resume_track_transfer_with_response + (rctx, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_MHD_make_json_pack ("{s:I, s:s}", + "code", + (json_int_t) + TALER_EC_TRACK_TRANSFER_REQUEST_ERROR, + "error", + "failed to run /transfers/ GET on exchange")); + } +} + + +/** + * Handle a timeout for the processing of the track transfer request. + * + * @param cls closure + */ +static void +handle_track_transfer_timeout (void *cls) +{ + struct TrackTransferContext *rctx = cls; + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming /track/transfer with error after timeout\n"); + rctx->timeout_task = NULL; + + if (NULL != rctx->fo) + { + TMH_EXCHANGES_find_exchange_cancel (rctx->fo); + rctx->fo = NULL; + } + resume_track_transfer_with_response (rctx, + MHD_HTTP_SERVICE_UNAVAILABLE, + TALER_MHD_make_error ( + TALER_EC_TRACK_TRANSFER_EXCHANGE_TIMEOUT, + "exchange not reachable")); +} + + +/** + * Function called with information about a wire transfer identifier. + * Generate a response based on the given @a proof. + * + * @param cls closure + * @param proof proof from exchange about what the wire transfer was for. + * should match the `TrackTransactionResponse` format + * of the exchange + */ +static void +proof_cb (void *cls, + const json_t *proof) +{ + struct TrackTransferContext *rctx = cls; + json_t *transformed_response; + + if (NULL == (transformed_response = + transform_response (proof, + rctx))) + { + rctx->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + rctx->response + = TALER_MHD_make_error (TALER_EC_TRACK_TRANSFER_JSON_RESPONSE_ERROR, + "Fail to elaborate response."); + return; + } + + rctx->response_code = MHD_HTTP_OK; + rctx->response = TALER_MHD_make_json (transformed_response); + json_decref (transformed_response); +} + + +/** + * Manages a /track/transfer call, thus it calls the /track/wtid + * offered by the exchange in order to return the set of transfers + * (of coins) associated with a given wire transfer. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] connection_cls the connection's closure (can be updated) + * @param upload_data upload data + * @param[in,out] upload_data_size number of bytes (left) in @a upload_data + * @param mi merchant backend instance, never NULL + * @return MHD result code + */ +MHD_RESULT +MH_handler_track_transfer (struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + void **connection_cls, + const char *upload_data, + size_t *upload_data_size, + struct MerchantInstance *mi) +{ + struct TrackTransferContext *rctx; + const char *str; + const char *url; + const char *wire_method; + MHD_RESULT ret; + enum GNUNET_DB_QueryStatus qs; + + if (NULL == *connection_cls) + { + rctx = GNUNET_new (struct TrackTransferContext); + rctx->hc.cc = &track_transfer_cleanup; + rctx->connection = connection; + *connection_cls = rctx; + } + else + { + /* not first call, recover state */ + rctx = *connection_cls; + } + + if (0 != rctx->response_code) + { + /* We are *done* processing the request, just queue the response (!) */ + if (UINT_MAX == rctx->response_code) + { + GNUNET_break (0); + return MHD_NO; /* hard error */ + } + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + if ( (NULL != rctx->fo) || + (NULL != rctx->eh) ) + { + /* likely old MHD version */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Not sure why we are here, should be suspended\n"); + return MHD_YES; /* still work in progress */ + } + + url = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "exchange"); + if (NULL == url) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "exchange"); + rctx->url = GNUNET_strdup (url); + + /* FIXME: change again: we probably don't want the wire_method + but rather the _account_ (section) here! */ + wire_method = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wire_method"); + if (NULL == wire_method) + { + if (1) + { + /* temporary work-around until demo is adjusted... */ + GNUNET_break (0); + wire_method = "x-taler-bank"; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Client needs fixing, see API change for #4943!\n"); + } + else + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wire_method"); + } + rctx->wire_method = GNUNET_strdup (wire_method); + rctx->mi = mi; + str = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "wtid"); + if (NULL == str) + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MISSING, + "wtid"); + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (str, + strlen (str), + &rctx->wtid, + sizeof (rctx->wtid))) + { + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_PARAMETER_MALFORMED, + "wtid"); + } + + /* Check if reply is already in database! */ + db->preflight (db->cls); + qs = db->find_proof_by_wtid (db->cls, + rctx->url, + &rctx->wtid, + &proof_cb, + rctx); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + 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); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_TRACK_TRANSFER_DB_FETCH_DEPOSIT_ERROR, + "Fail to query database about proofs"); + } + if (0 != rctx->response_code) + { + ret = MHD_queue_response (connection, + rctx->response_code, + rctx->response); + if (NULL != rctx->response) + { + MHD_destroy_response (rctx->response); + rctx->response = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Queueing response (%u) for /track/transfer (%s).\n", + (unsigned int) rctx->response_code, + ret ? "OK" : "FAILED"); + return ret; + } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Suspending /track/transfer handling while working with the exchange\n"); + MHD_suspend_connection (connection); + rctx->fo = TMH_EXCHANGES_find_exchange (url, + NULL, + GNUNET_NO, + &process_track_transfer_with_exchange, + rctx); + rctx->timeout_task + = GNUNET_SCHEDULER_add_delayed (TRACK_TIMEOUT, + &handle_track_transfer_timeout, + rctx); + return MHD_YES; +} + + +/* end of taler-merchant-httpd_track-transfer.c */ diff --git a/src/backend/taler-merchant-httpd_tip-reserve-helper.h b/src/backend/taler-merchant-httpd_reserves_get.h index f180546d..f180546d 100644 --- a/src/backend/taler-merchant-httpd_tip-reserve-helper.h +++ b/src/backend/taler-merchant-httpd_reserves_get.h diff --git a/src/backend/taler-merchant-httpd_tip-query.h b/src/backend/taler-merchant-httpd_reserves_reserve_get.h index 3123486c..3123486c 100644 --- a/src/backend/taler-merchant-httpd_tip-query.h +++ b/src/backend/taler-merchant-httpd_reserves_reserve_get.h diff --git a/src/backend/taler-merchant-httpd_refund.c b/src/backend/taler-merchant-httpd_responses.c index 62ebf451..62ebf451 100644 --- a/src/backend/taler-merchant-httpd_refund.c +++ b/src/backend/taler-merchant-httpd_responses.c diff --git a/src/backend/taler-merchant-httpd_tip-authorize.h b/src/backend/taler-merchant-httpd_tips_post.h index 1f7f44ea..1f7f44ea 100644 --- a/src/backend/taler-merchant-httpd_tip-authorize.h +++ b/src/backend/taler-merchant-httpd_tips_post.h diff --git a/src/backend/taler-merchant-httpd_tip-pickup.h b/src/backend/taler-merchant-httpd_tips_tip_pickup.h index 6fdba31a..6fdba31a 100644 --- a/src/backend/taler-merchant-httpd_tip-pickup.h +++ b/src/backend/taler-merchant-httpd_tips_tip_pickup.h diff --git a/src/backend/taler-merchant-httpd_track-transfer.h b/src/backend/taler-merchant-httpd_transfers-post.h index 0463295e..0463295e 100644 --- a/src/backend/taler-merchant-httpd_track-transfer.h +++ b/src/backend/taler-merchant-httpd_transfers-post.h diff --git a/src/backenddb/merchant-0001.sql b/src/backenddb/merchant-0001.sql index 1bd35c35..2acf49cd 100644 --- a/src/backenddb/merchant-0001.sql +++ b/src/backenddb/merchant-0001.sql @@ -17,24 +17,23 @@ -- Everything in one big transaction BEGIN; --- TODO: consider adding BIGSERIAL primary keys on many of the tables! - -- Check patch versioning is in place. SELECT _v.register_patch('merchant-0001', NULL, NULL); ---------------- Exchange information --------------------------- CREATE TABLE IF NOT EXISTS merchant_exchange_wire_fees - (master_pub BYTEA NOT NULL CHECK (LENGTH(master_pub)=32) + (wirefee_serial BIGSERIAL PRIMARY KEY + ,master_pub BYTEA NOT NULL CHECK (LENGTH(master_pub)=32) ,h_wire_method BYTEA NOT NULL CHECK (LENGTH(h_wire_method)=64) + ,start_date INT8 NOT NULL + ,end_date INT8 NOT NULL ,wire_fee_val INT8 NOT NULL ,wire_fee_frac INT4 NOT NULL ,closing_fee_val INT8 NOT NULL ,closing_fee_frac INT4 NOT NULL - ,start_date INT8 NOT NULL - ,end_date INT8 NOT NULL ,master_sig BYTEA NOT NULL CHECK (LENGTH(master_sig)=64) - ,PRIMARY KEY (exchange_pub,h_wire_method,start_date,end_date) + ,UNIQUE (master_pub,h_wire_method,start_date) ); COMMENT ON TABLE merchant_exchange_wire_fees IS 'Here we store proofs of the wire fee structure of the various exchanges'; @@ -42,13 +41,14 @@ COMMENT ON COLUMN merchant_exchange_wire_fees.master_pub IS 'Master public key of the exchange with these wire fees'; CREATE TABLE IF NOT EXISTS merchant_exchange_signing_keys - (master_pub BYTEA NOT NULL CHECK (LENGTH(master_pub)=32) + (signkey_serial BIGSERIAL PRIMARY KEY + ,master_pub BYTEA NOT NULL CHECK (LENGTH(master_pub)=32) ,exchange_pub BYTEA NOT NULL CHECK (LENGTH(exchange_pub)=32) ,start_date INT8 NOT NULL ,expire_date INT8 NOT NULL ,end_date INT8 NOT NULL - ,master_sig BYTEA NOT NULL CHECK (LENGTH(master_sig)=64) - ,PRIMARY KEY (master_pub,exchange_pub) + ,master_sig BYTEA NOT NULL CHECK (LENGTH(master_sig)=64), + UNIQUE (master_pub, exchange_pub, start_date) ); COMMENT ON TABLE merchant_exchange_signing_keys IS 'Here we store proofs of the exchange online signing keys being signed by the exchange master key'; @@ -59,11 +59,11 @@ COMMENT ON COLUMN merchant_exchange_signing_keys.master_pub -------------------------- Instances --------------------------- CREATE TABLE IF NOT EXISTS merchant_instances - (merchant_pub BYTEA NOT NULL CHECK (LENGTH(merchant_pub)=32), + (merchant_serial BIGSERIAL PRIMARY KEY + ,merchant_pub BYTEA NOT NULL UNIQUE CHECK (LENGTH(merchant_pub)=32) ,merchant_name VARCHAR NOT NULL ,location BYTEA NOT NULL ,jurisdiction BYTEA NOT NULL - ,PRIMARY KEY (reserve_pub) ); COMMENT ON TABLE merchant_instances IS 'all the instances supported by this backend'; @@ -75,43 +75,42 @@ COMMENT ON COLUMN merchant_instances.jurisdiction IS 'jurisdiction of the merchant as a Location in JSON format (required)'; CREATE TABLE IF NOT EXISTS merchant_keys - (merchant_priv BYTEA NOT NULL CHECK (LENGTH(merchant_priv)=32) UNIQUE, - merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE - ,PRIMARY KEY (merchant_pub) + (merchant_priv BYTEA NOT NULL UNIQUE CHECK (LENGTH(merchant_priv)=32), + merchant_serial BIGINT PRIMARY KEY + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE ); COMMENT ON TABLE merchant_keys IS 'private keys of instances that have not been deleted'; CREATE TABLE IF NOT EXISTS merchant_instance_accounts - (account_id BIGSERIAL NOT NULL - merchant_pub BYTEA NOT NULL CHECK (LENGTH(merchant_pub)=32), - ,h_wire BYTEA NOT NULL CHECK (LENGTH(h_wire)=64), -- or did we use a shorter hash here? - ,salt BYTEA NOT NULL CHECK (LENGTH(salt)=64), -- or did we use a shorter salt here? + (account_serial BIGSERIAL PRIMARY KEY + ,merchant_serial BIGINT NOT NULL UNIQUE + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE + ,h_wire BYTEA NOT NULL CHECK (LENGTH(h_wire)=64) ,active boolean NOT NULL - ,payto_uri VARCHAR NOT NULL CHECK, - ,PRIMARY KEY (merchant_pub,h_wire) - ,FOREIGN KEY (merchant_pub) - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + ,salt VARCHAR NOT NULL + ,payto_uri VARCHAR NOT NULL + ,UNIQUE (merchant_serial,payto_uri) ); -COMMENT ON TABLE merchant_accounts +COMMENT ON TABLE merchant_instance_accounts IS 'bank accounts of the instances'; -COMMENT ON COLUMN merchant_accounts.h_wire +COMMENT ON COLUMN merchant_instance_accounts.h_wire IS 'salted hash of payto_uri'; -COMMENT ON COLUMN merchant_accounts.salt +COMMENT ON COLUMN merchant_instance_accounts.salt IS 'salt used when hashing payto_uri into h_wire'; -COMMENT ON COLUMN merchant_accounts.payto_uri +COMMENT ON COLUMN merchant_instance_accounts.payto_uri IS 'payto URI of a merchant bank account'; -COMMENT ON COLUMN merchant_instances.active +COMMENT ON COLUMN merchant_instance_accounts.active IS 'true if we actively use this bank account, false if it is just kept around for older contracts to refer to'; -------------------------- Inventory --------------------------- CREATE TABLE IF NOT EXISTS merchant_inventory - (product_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (product_serial BIGSERIAL PRIMARY KEY + ,merchant_serial BIGINT NOT NULL + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE + ,product_id VARCHAR NOT NULL ,description VARCHAR NOT NULL ,description_i18n BYTEA NOT NULL ,unit VARCHAR NOT NULL @@ -124,7 +123,7 @@ CREATE TABLE IF NOT EXISTS merchant_inventory ,total_lost BIGINT NOT NULL ,location BYTEA NOT NULL ,next_restock INT8 NOT NULL - ,PRIMARY KEY (product_id, merchant_pub) + ,UNIQUE (merchant_serial, product_id) ); COMMENT ON TABLE merchant_inventory IS 'products offered by the merchant (may be incomplete, frontend can override)'; @@ -152,38 +151,42 @@ COMMENT ON COLUMN merchant_inventory.next_restock IS 'GNUnet absolute time indicating when the next restock is expected. 0 for unknown.'; CREATE TABLE IF NOT EXISTS merchant_inventory_locks - (product_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE - ,lock_uuid BYTEA NOT NULL -- FIXME: length constraint? + (product_serial BIGINT NOT NULL + REFERENCES merchant_inventory (product_serial) ON DELETE CASCADE + ,lock_uuid BYTEA NOT NULL CHECK (LENGTH(lock_uuid)=32) ,total_locked BIGINT NOT NULL - ,expiration TIMESTAMP NOT NULL, - ,FOREIGN KEY (product_id, merchant_pub) - REFERENCES merchant_inventory (product_id, merchant_pub) ON DELETE CASCADE + ,expiration TIMESTAMP NOT NULL ); +CREATE INDEX IF NOT EXISTS merchant_inventory_locks_by_product_and_lock + ON merchant_inventory_locks + (product_serial, lock_uuid); +CREATE INDEX IF NOT EXISTS merchant_inventory_locks_by_expiration + ON merchant_inventory_locks + (expiration); COMMENT ON TABLE merchant_inventory_locks IS 'locks on inventory helt by shopping carts'; -COMMENT ON TABLE merchant_inventory_locks.total_locked +COMMENT ON COLUMN merchant_inventory_locks.total_locked IS 'how many units of the product does this lock reserve'; -COMMENT ON TABLE merchant_inventory_locks.expiration +COMMENT ON COLUMN merchant_inventory_locks.expiration IS 'when does this lock automatically expire (if no order is created)'; ---------------- Orders and contracts --------------------------- CREATE TABLE IF NOT EXISTS merchant_orders - (order_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (order_serial BIGSERIAL PRIMARY KEY + ,merchant_serial BIGINT NOT NULL + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE + ,order_id VARCHAR NOT NULL ,contract_terms BYTEA NOT NULL ,pay_deadline INT8 NOT NULL - ,PRIMARY KEY (order_id, merchant_pub) + ,UNIQUE (merchant_serial, order_id) ); COMMENT ON TABLE merchant_orders IS 'Orders we offered to a customer, but that have not yet been claimed'; -COMMENT ON COLUMN merchnat_orders.contract_terms +COMMENT ON COLUMN merchant_orders.contract_terms IS 'Claiming changes the contract_terms, hence we have no hash of the terms in this table'; -COMMENT ON COLUMN merchant_orders.merchant_pub +COMMENT ON COLUMN merchant_orders.merchant_serial IS 'Identifies the instance offering the contract'; COMMENT ON COLUMN merchant_orders.pay_deadline IS 'How long is the offer valid. After this time, the order can be garbage collected'; @@ -192,40 +195,40 @@ CREATE INDEX IF NOT EXISTS merchant_orders_by_expiration (pay_deadline); CREATE TABLE IF NOT EXISTS merchant_order_locks - (product_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (product_serial BIGINT NOT NULL + REFERENCES merchant_inventory (product_serial) ON DELETE CASCADE ,total_locked BIGINT NOT NULL - ,order_id VARCHAR NOT NULL, - ,FOREIGN KEY (order_id, merchant_pub) - REFERENCES merchant_orders (order_id, merchant_pub) ON DELETE CASCADE - ,FOREIGN KEY (product_id, merchant_pub) - REFERENCES merchant_inventory (product_id, merchant_pub) ON DELETE CASCADE - ,PRIMARY KEY (product_id, merchant_pub, order_id) + ,order_serial BIGINT NOT NULL + REFERENCES merchant_orders (order_serial) ON DELETE CASCADE ); -COMMENT ON TABLE merchant_inventory_locks +CREATE INDEX IF NOT EXISTS merchant_orders_locks_by_order_and_product + ON merchant_order_locks + (order_serial, product_serial); +COMMENT ON TABLE merchant_order_locks IS 'locks on orders awaiting claim and payment'; -COMMENT ON TABLE merchant_inventory_locks.total_locked +COMMENT ON COLUMN merchant_order_locks.total_locked IS 'how many units of the product does this lock reserve'; CREATE TABLE IF NOT EXISTS merchant_contract_terms - (order_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (contract_serial BIGSERIAL PRIMARY KEY + ,merchant_serial BIGINT NOT NULL + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE + ,contract_id VARCHAR NOT NULL ,contract_terms BYTEA NOT NULL ,h_contract_terms BYTEA NOT NULL CHECK (LENGTH(h_contract_terms)=64) ,pay_deadline INT8 NOT NULL - ,row_id BIGSERIAL UNIQUE - ,paid boolean DEFAULT FALSE NOT NULL ,refund_deadline INT8 NOT NULL - ,PRIMARY KEY (order_id, merchant_pub) - ,UNIQUE (h_contract_terms, merchant_pub) + ,paid BOOLEAN DEFAULT FALSE NOT NULL + ,fulfillment_url VARCHAR NOT NULL + ,session_id VARCHAR NOT NULL + ,UNIQUE (merchant_serial, contract_id) + ,UNIQUE (merchant_serial, h_contract_terms) ); COMMENT ON TABLE merchant_contract_terms IS 'Contracts are orders that have been claimed by a wallet'; -COMMENT ON COLUMN merchant_contract_terms.order_id +COMMENT ON COLUMN merchant_contract_terms.contract_id IS 'Not a foreign key into merchant_orders because paid contracts persist after expiration'; -COMMENT ON COLUMN merchant_contract_terms.merchant_pub +COMMENT ON COLUMN merchant_contract_terms.merchant_serial IS 'Identifies the instance offering the contract'; COMMENT ON COLUMN merchant_contract_terms.contract_terms IS 'These contract terms include the wallet nonce'; @@ -235,19 +238,29 @@ COMMENT ON COLUMN merchant_contract_terms.refund_deadline IS 'By what times do refunds have to be approved (useful to reject refund requests)'; COMMENT ON COLUMN merchant_contract_terms.paid IS 'true implies the customer paid for this contract; order should be DELETEd from merchant_orders once paid is set to release merchant_order_locks; paid remains true even if the payment was later refunded'; +COMMENT ON COLUMN merchant_contract_terms.fulfillment_url + IS 'also included in contract_terms, but we need it here to SELECT on it during repurchase detection'; +COMMENT ON COLUMN merchant_contract_terms.session_id + IS 'last session_id from we confirmed the paying client to use, empty string for none'; COMMENT ON COLUMN merchant_contract_terms.pay_deadline IS 'How long is the offer valid. After this time, the order can be garbage collected'; -CREATE INDEX IF NOT EXISTS merchant_contract_terms_by_expiration +CREATE INDEX IF NOT EXISTS merchant_contract_terms_by_merchant_and_expiration + ON merchant_contract_terms + (merchant_serial,pay_deadline); +CREATE INDEX IF NOT EXISTS merchant_contract_terms_by_merchant_and_payment ON merchant_contract_terms - (pay_deadline); + (merchant_serial,paid); +CREATE INDEX IF NOT EXISTS merchant_contract_terms_by_merchant_session_and_fulfillment + ON merchant_contract_terms + (merchant_serial,fulfillment_url,session_id); ---------------- Payment and refunds --------------------------- CREATE TABLE IF NOT EXISTS merchant_deposits - (h_contract_terms BYTEA NOT NULL - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (deposit_serial BIGSERIAL PRIMARY KEY + ,contract_serial BIGINT + REFERENCES merchant_contract_terms (contract_serial) ON DELETE CASCADE ,coin_pub BYTEA NOT NULL CHECK (LENGTH(coin_pub)=32) ,exchange_url VARCHAR NOT NULL ,amount_with_fee_val INT8 NOT NULL @@ -258,107 +271,61 @@ CREATE TABLE IF NOT EXISTS merchant_deposits ,refund_fee_frac INT4 NOT NULL ,wire_fee_val INT8 NOT NULL ,wire_fee_frac INT4 NOT NULL - ,exchange_pub BYTEA NOT NULL - REFERENCES merchant_exchange_signing_keys (exchange_pub) ON DELETE CASCADE + ,signkey_serial BIGINT NOT NULL + REFERENCES merchant_exchange_signing_keys (signkey_serial) ON DELETE CASCADE ,exchange_sig BYTEA NOT NULL CHECK (LENGTH(exchange_sig)=64) - ,exchange_timestamp INT 8 NOT NULL - ,h_wire BYTEA NOT NULL - ,exchange_proof BYTEA NOT NULL - ,PRIMARY KEY (h_contract_terms, coin_pub) - ,FOREIGN KEY (h_contract_terms, merchant_pub) - REFERENCES merchant_contract_terms (h_contract_terms, merchant_pub) ON DELETE CASCADE - ,FOREIGN KEY (h_wire, merchant_pub) - REFERENCES merchant_instance_accounts (h_wire, merchant_pub) ON DELETE CASCADE + ,exchange_timestamp INT8 NOT NULL + ,account_serial BIGINT NOT NULL + REFERENCES merchant_instance_accounts (account_serial) ON DELETE CASCADE + ,UNIQUE (contract_serial, coin_pub) ); COMMENT ON TABLE merchant_deposits IS 'Table with the deposit confirmations for each coin we deposited at the exchange'; -COMMENT ON COLUMN merchant_deposits.exchange_pub +COMMENT ON COLUMN merchant_deposits.signkey_serial IS 'Online signing key of the exchange on the deposit confirmation'; COMMENT ON COLUMN merchant_deposits.exchange_sig IS 'Signature of the exchange over the deposit confirmation'; COMMENT ON COLUMN merchant_deposits.wire_fee_val IS 'We MAY want to see if we should try to get this via merchant_exchange_wire_fees (not sure, may be too complicated with the date range, etc.)'; -COMMENT ON COLUMN merchant_deposits.transferred - -CREATE TABLE IF NOT EXISTS merchant_session_info - (session_id VARCHAR NOT NULL - ,fulfillment_url VARCHAR NOT NULL --- FIXME: why do we store this here? - ,order_id VARCHAR NOT NULL - ,merchant_pub BYTEA NOT NULL CHECK (LENGTH(merchant_pub)=32) - ,timestamp INT8 NOT NULL - ,PRIMARY KEY (session_id, fulfillment_url, merchant_pub) --- FIXME: I am confused why this even *IS* a primary key. - ,FOREIGN KEY (order_id, merchant_pub) - REFERENCES merchant_orders (order_id, merchant_pub) --- FIXME: if this is for session-bound payments, --- we need to reference merchant_contracts as --- the *order* may be GCed after payment but the --- session-bound payment mechanism should continue to work! - ,UNIQUE (session_id, fulfillment_url, order_id, merchant_pub) --- FIXME: isn't this redundant with the (confusing) PRIMARY KEY? - ); --- FIXME: Support for #5853 (limit session in number and duration) --- should be supported 'somewhere' here. --- => #5853 suggests a UNIQUE constraint on order_id+merchant_pub on this table! -COMMENT ON TABLE merchant_session_info - IS 'sessions and their order_id/fulfillment_url mapping'; -COMMENT ON COLUMN merchant_session_info.fulfillment_url - IS 'FIXME: Unclear why the fulfillment URL is in this table'; -COMMENT ON COLUMN merchant_session_info.order_id - IS 'FIXME: Why use merchant_pub+order_id here, instead of a say a contract_id?'; CREATE TABLE IF NOT EXISTS merchant_refunds - (rtransaction_id BIGSERIAL UNIQUE - ,merchant_pub BYTEA NOT NULL - ,h_contract_terms BYTEA NOT NULL + (refund_serial BIGSERIAL PRIMARY KEY + ,contract_serial BIGINT NOT NULL + REFERENCES merchant_contract_terms (contract_serial) ON DELETE CASCADE + ,rtransaction_id BIGINT NOT NULL ,coin_pub BYTEA NOT NULL ,reason VARCHAR NOT NULL ,refund_amount_val INT8 NOT NULL ,refund_amount_frac INT4 NOT NULL - ,FOREIGN KEY (h_contract_terms, coin_pub) - REFERENCES merchant_deposits (h_contract_terms, coin_pub) ON DELETE CASCADE - ,FOREIGN KEY (h_contract_terms, merchant_pub) - REFERENCES merchant_contract_terms (h_contract_terms, merchant_pub) ON DELETE CASCADE - ,PRIMARY KEY (h_contract_terms, merchant_pub, coin_pub, rtransaction_id) + ,UNIQUE (contract_serial, coin_pub, rtransaction_id) ); COMMENT ON TABLE merchant_deposits IS 'Refunds approved by the merchant (backoffice) logic, excludes abort refunds'; COMMENT ON COLUMN merchant_refunds.rtransaction_id IS 'Needed for uniqueness in case a refund is increased for the same order'; --- FIXME: do we really want rtransaction_id as BIGSERIAL UNIQUE? --- this exposes # of refunds granted to clients!!! CREATE TABLE IF NOT EXISTS merchant_refund_proofs - (rtransaction_id BIGSERIAL UNIQUE - ,merchant_pub BYTEA NOT NULL CHECK (LENGTH(merchant_pub)=32) - ,h_contract_terms BYTEA NOT NULL CHECK (LENGTH(h_contract_terms)=64) - ,coin_pub BYTEA NOT NULL CHECK (LENGTH(coin_pub)=32) + (refund_serial BIGINT PRIMARY KEY + REFERENCES merchant_refunds (refund_serial) ON DELETE CASCADE ,exchange_sig BYTEA NOT NULL CHECK (LENGTH(exchange_sig)=64) - ,exchange_pub BYTEA NOT NULL - REFERENCES merchant_exchange_signing_keys (exchange_pub) ON DELETE CASCADE - ,FOREIGN KEY (h_contract_terms, merchant_pub, coin_pub, rtransaction_id) - REFERENCES merchant_refunds (h_contract_terms, merchant_pub, coin_pub, rtransaction_id) ON DELETE CASCADE - ,PRIMARY KEY (h_contract_terms, merchant_pub, coin_pub, rtransaction_id) + ,signkey_serial BIGINT NOT NULL + REFERENCES merchant_exchange_signing_keys (signkey_serial) ON DELETE CASCADE ); COMMENT ON TABLE merchant_refund_proofs IS 'Refunds confirmed by the exchange (not all approved refunds are grabbed by the wallet)'; --- FIXME: rtransaction_id as BIGSERIAL UNIQUE should suffice, rest of information --- in the FOREIGN KEY is duplicated for no good reason. - -------------------- Wire transfers --------------------------- CREATE TABLE IF NOT EXISTS merchant_credits - (credit_serial BIGSERIAL NOT NULL + (credit_serial BIGSERIAL PRIMARY KEY ,exchange_url VARCHAR NOT NULL ,wtid BYTEA CHECK (LENGTH(wtid)=32) ,credit_amount_val INT8 NOT NULL ,credit_amount_frac INT4 NOT NULL - ,account_id BIGSERIAL NOT NULL - REFERENCES merchant_instance_accounts (account_id) ON DELETE CASCADE + ,account_serial BIGINT NOT NULL + REFERENCES merchant_instance_accounts (account_serial) ON DELETE CASCADE ,verified BOOLEAN NOT NULL DEFAULT FALSE - ,PRIMARY KEY (wtid, exchange_url) + ,UNIQUE (wtid, exchange_url) ); COMMENT ON TABLE merchant_credits IS 'table represents the information provided by the (trusted) merchant about incoming wire transfers'; @@ -366,60 +333,47 @@ COMMENT ON COLUMN merchant_credits.verified IS 'true once we got an acceptable response from the exchange for this transfer'; CREATE TABLE IF NOT EXISTS merchant_transfer_signatures - (credit_serial BIGSERIAL NOT NULL + (credit_serial BIGINT PRIMARY KEY + REFERENCES merchant_credits (credit_serial) ON DELETE CASCADE + ,account_serial BIGINT NOT NULL + REFERENCES merchant_instance_accounts (account_serial) ON DELETE CASCADE + ,signkey_serial BIGINT NOT NULL + REFERENCES merchant_exchange_signing_keys (signkey_serial) ON DELETE CASCADE ,execution_time INT8 NOT NULL - ,exchange_pub BYTEA NOT NULL - REFERENCES merchant_exchange_signing_keys (exchange_pub) ON DELETE CASCADE ,exchange_sig BYTEA NOT NULL CHECK (LENGTH(exchange_sig)=64) - ,h_wire BYTEA NOT NULL CHECK (LENGTH(h_wire)=64) - ,PRIMARY KEY (credit_serial) - ,FOREIGN KEY (credit_serial) - REFERENCES merchant_credits (credit_serial) ON DELETE CASCADE ); COMMENT ON TABLE merchant_transfer_signatures IS 'table represents the main information returned from the /transfer request to the exchange.'; -COMMENT ON COLUMN merchant_transfer_signatures.h_wire - IS 'salted hash of the merchant bank account'; CREATE TABLE IF NOT EXISTS merchant_transfer_by_coin - (h_contract_terms BYTEA NOT NULL - ,coin_pub BYTEA NOT NULL CHECK (LENGTH(coin_pub)=32) + (deposit_serial BIGINT UNIQUE NOT NULL + REFERENCES merchant_deposits (deposit_serial) ON DELETE CASCADE + ,credit_serial BIGINT NOT NULL + REFERENCES merchant_credits (credit_serial) ON DELETE CASCADE ,offset_in_exchange_list INT8 NOT NULL - ,credit_serial BIGSERIAL NOT NULL - REFERENCES merchant_credits (credit_serial) ON DELETE CASCADE ,exchange_deposit_value_val INT8 NOT NULL ,exchange_deposit_value_frac INT4 NOT NULL ,exchange_deposit_fee_val INT8 NOT NULL ,exchange_deposit_fee_frac INT4 NOT NULL - ,PRIMARY KEY (h_contract_terms, coin_pub) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS merchant_transfers_by_coin - ON merchant_transfers - (h_contract_terms - ,coin_pub); -CREATE INDEX IF NOT EXISTS merchant_transfers_by_wtid - ON merchant_transfers - (wtid); -COMMENT ON TABLE merchant_transfers +CREATE INDEX IF NOT EXISTS merchant_transfers_by_credit + ON merchant_transfer_by_coin + (credit_serial); +COMMENT ON TABLE merchant_transfer_by_coin IS 'Mapping of deposits to wire transfers and vice versa'; -COMMENT ON COLUMN merchant_transfers.coin_pub - IS 'h_contract_terms and coin_pub are not a FOREIGN KEY into merchant_deposits because theoretically the exchange could pay us for things we are not aware of having sold. We still need to store this to check the signatures. This is also the reason why the deposit value and fees are duplicated here: this is about checkability of signatures. We may disagree with the claims of the exchange, but we still need the proof of what the exchange said.'; -COMMENT ON COLUMN merchant_transfers.exchange_deposit_value_val +COMMENT ON COLUMN merchant_transfer_by_coin.exchange_deposit_value_val IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits minus refunds'; -COMMENT ON COLUMN merchant_transfers.exchange_deposit_fee_val +COMMENT ON COLUMN merchant_transfer_by_coin.exchange_deposit_fee_val IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits'; -COMMENT ON COLUMN merchant_transfers.offset_in_exchange_list - IS 'At which offset in the exchange list for the wire transfer (under "deposits") does this coin appear'; -COMMENT ON COLUMN merchant_transfers.coin_pub - IS 'Note that the coin_pub/h_contract_terms can theoretically be unknown to us if someone else deposited for us at the exchange. Hence those cannot be foreign keys into the merchant_deposits table.'; -------------------------- Tipping --------------------------- CREATE TABLE IF NOT EXISTS merchant_tip_reserves - (reserve_pub BYTEA NOT NULL CHECK (LENGTH(reserve_pub)=32) - ,merchant_pub BYTEA NOT NULL - REFERENCES merchant_instances (merchant_pub) ON DELETE CASCADE + (reserve_serial BIGSERIAL PRIMARY KEY + ,reserve_pub BYTEA NOT NULL UNIQUE CHECK (LENGTH(reserve_pub)=32) + ,merchant_serial BIGINT NOT NULL + REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE ,expiration INT8 NOT NULL ,merchant_initial_balance_val INT8 NOT NULL ,merchant_initial_balance_frac INT4 NOT NULL @@ -429,7 +383,6 @@ CREATE TABLE IF NOT EXISTS merchant_tip_reserves ,tips_committed_frac INT4 NOT NULL ,tips_picked_up_val INT8 NOT NULL ,tips_picked_up_frac INT4 NOT NULL - ,PRIMARY KEY (reserve_pub) ); COMMENT ON TABLE merchant_tip_reserves IS 'balances of the reserves available for tips'; @@ -445,19 +398,19 @@ COMMENT ON COLUMN merchant_tip_reserves.tips_picked_up_val IS 'Total amount tips that have been picked up from this reserve'; CREATE TABLE IF NOT EXISTS merchant_tip_reserve_kreys - (reserve_priv BYTEA NOT NULL CHECK (LENGTH(reserve_priv)=32) UNIQUE - ,reserve_pub BYTEA NOT NULL - REFERENCES merchant_tip_reserves (reserve_pub) ON DELETE CASCADE + (reserve_serial BIGINT NOT NULL UNIQUE + REFERENCES merchant_tip_reserves (reserve_serial) ON DELETE CASCADE + ,reserve_priv BYTEA NOT NULL UNIQUE CHECK (LENGTH(reserve_priv)=32) ,exchange_url VARCHAR NOT NULL - ,PRIMARY KEY (reserve_pub) ); -COMMENT ON TABLE merchant_tip_reserve_keys +COMMENT ON TABLE merchant_tip_reserves IS 'private keys of reserves that have not been deleted'; CREATE TABLE IF NOT EXISTS merchant_tips - (reserve_pub BYTEA NOT NULL - REFERENCES merchant_tip_reserves (reserve_pub) ON DELETE CASCADE - ,tip_id BYTEA NOT NULL CHECK (LENGTH(tip_id)=64) + (tip_serial BIGSERIAL PRIMARY KEY + ,reserve_serial BIGINT NOT NULL UNIQUE + REFERENCES merchant_tip_reserves (reserve_serial) ON DELETE CASCADE + ,tip_id BYTEA NOT NULL UNIQUE CHECK (LENGTH(tip_id)=64) ,justification VARCHAR NOT NULL ,expiration INT8 NOT NULL ,amount_val INT8 NOT NULL @@ -465,28 +418,27 @@ CREATE TABLE IF NOT EXISTS merchant_tips ,left_val INT8 NOT NULL ,left_frac INT4 NOT NULL ,was_picked_up BOOLEAN NOT NULL DEFAULT FALSE - ,PRIMARY KEY (tip_id) ); +CREATE INDEX IF NOT EXISTS merchant_tips_by_pickup_and_expiration + ON merchant_tips + (was_picked_up,expiration); COMMENT ON TABLE merchant_tips IS 'tips that have been authorized'; COMMENT ON COLUMN merchant_tips.amount_val IS 'Overall tip amount'; COMMENT ON COLUMN merchant_tips.left_val IS 'Tip amount not yet picked up'; -COMMENT ON COLUMN merchant_tips.reserve_pub +COMMENT ON COLUMN merchant_tips.reserve_serial IS 'Reserve from which this tip is funded'; COMMENT ON COLUMN merchant_tips.expiration IS 'time by which the wallet has to pick up the tip before it expires'; -CREATE INDEX IF NOT EXISTS merchant_tips_by_expiration - ON merchant_tips - (expiration); CREATE TABLE IF NOT EXISTS merchant_tip_pickups - (tip_id BYTEA NOT NULL REFERENCES merchant_tips (tip_id) ON DELETE CASCADE - ,pickup_id BYTEA NOT NULL CHECK (LENGTH(pickup_id)=64) + (tip_serial BIGINT NOT NULL + REFERENCES merchant_tips (tip_serial) ON DELETE CASCADE + ,pickup_id BYTEA NOT NULL UNIQUE CHECK (LENGTH(pickup_id)=64) ,amount_val INT8 NOT NULL ,amount_frac INT4 NOT NULL - ,PRIMARY KEY (pickup_id) ); COMMENT ON TABLE merchant_tip_pickups IS 'tips that have been picked up'; @@ -494,7 +446,8 @@ COMMENT ON COLUMN merchant_tips.amount_val IS 'total transaction cost for all coins including withdraw fees'; CREATE TABLE IF NOT EXISTS merchant_tip_pickup_signatures - (pickup_id BYTEA NOT NULL REFERENCES merchant_tip_pickups (pickup_id) ON DELETE CASCADE + (pickup_id BYTEA NOT NULL + REFERENCES merchant_tip_pickups (pickup_id) ON DELETE CASCADE ,coin_offset INT4 NOT NULL ,blind_sig BYTEA NOT NULL ,PRIMARY KEY (pickup_id, coin_offset) diff --git a/src/merchant-tools/taler-merchant-benchmark.c b/src/merchant-tools/taler-merchant-benchmark.c index dc7f933d..921102c5 100644 --- a/src/merchant-tools/taler-merchant-benchmark.c +++ b/src/merchant-tools/taler-merchant-benchmark.c @@ -509,7 +509,7 @@ run (void *cls, * * @param process process to terminate. */ -void +static void terminate_process (struct GNUNET_OS_Process *process) { GNUNET_OS_process_kill (process, SIGTERM); |