From 2056bc82f9aac337a9c7683cdb0aa43debd4d50c Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 29 Jul 2022 09:57:10 +0200 Subject: [PATCH] expand taler-exchange-offline and libtalerexchange with management-drain-profits implementation (#4960) --- src/exchange-tools/taler-exchange-offline.c | 283 ++++++++++++++++++ src/include/taler_exchange_service.h | 57 ++++ src/lib/Makefile.am | 1 + .../exchange_api_management_drain_profits.c | 213 +++++++++++++ 4 files changed, 554 insertions(+) create mode 100644 src/lib/exchange_api_management_drain_profits.c diff --git a/src/exchange-tools/taler-exchange-offline.c b/src/exchange-tools/taler-exchange-offline.c index 41cd2597e..d6245b3ff 100644 --- a/src/exchange-tools/taler-exchange-offline.c +++ b/src/exchange-tools/taler-exchange-offline.c @@ -107,6 +107,10 @@ */ #define OP_EXTENSIONS "exchange-extensions-0" +/** + * Generate message to drain profits. + */ +#define OP_DRAIN_PROFITS "exchange-drain-profits-0" /** * Our private key, initialized in #load_offline_key(). @@ -384,6 +388,34 @@ struct WireFeeRequest }; +/** + * Data structure for draining profits. + */ +struct DrainProfitsRequest +{ + + /** + * Kept in a DLL. + */ + struct DrainProfitsRequest *next; + + /** + * Kept in a DLL. + */ + struct DrainProfitsRequest *prev; + + /** + * Operation handle. + */ + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *h; + + /** + * Array index of the associated command. + */ + size_t idx; +}; + + /** * Data structure for announcing global fees. */ @@ -575,6 +607,17 @@ static struct UploadExtensionsRequest *uer_head; */ static struct UploadExtensionsRequest *uer_tail; +/** + * Active drain profits requests. + */ +struct DrainProfitsRequest *dpr_head; + +/** + * Active drain profits requests. + */ +static struct DrainProfitsRequest *dpr_tail; + + /** * Shutdown task. Invoked when the application is being terminated. * @@ -736,6 +779,23 @@ do_shutdown (void *cls) GNUNET_free (uer); } } + + { + struct DrainProfitsRequest *dpr; + + while (NULL != (dpr = dpr_head)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Aborting incomplete drain profits request #%u\n", + (unsigned int) dpr->idx); + TALER_EXCHANGE_management_drain_profits_cancel (dpr->h); + GNUNET_CONTAINER_DLL_remove (dpr_head, + dpr_tail, + dpr); + GNUNET_free (dpr); + } + } + if (NULL != out) { json_dumpf (out, @@ -790,6 +850,7 @@ test_shutdown (void) (NULL == gfr_head) && (NULL == ukr_head) && (NULL == uer_head) && + (NULL == dpr_head) && (NULL == mgkh) && (NULL == nxt) ) GNUNET_SCHEDULER_shutdown (); @@ -1749,6 +1810,112 @@ upload_global_fee (const char *exchange_url, } +/** + * Function called with information about the drain profits operation. + * + * @param cls closure with a `struct DrainProfitsRequest` + * @param hr HTTP response data + */ +static void +drain_profits_cb ( + void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr) +{ + struct DrainProfitsRequest *dpr = cls; + + if (MHD_HTTP_NO_CONTENT != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Upload failed for command %u with status %u: %s (%s)\n", + (unsigned int) dpr->idx, + hr->http_status, + TALER_ErrorCode_get_hint (hr->ec), + hr->hint); + global_ret = EXIT_FAILURE; + } + GNUNET_CONTAINER_DLL_remove (dpr_head, + dpr_tail, + dpr); + GNUNET_free (dpr); + test_shutdown (); +} + + +/** + * Upload drain profit action. + * + * @param exchange_url base URL of the exchange + * @param idx index of the operation we are performing (for logging) + * @param value arguments for drain profits + */ +static void +upload_drain (const char *exchange_url, + size_t idx, + const json_t *value) +{ + struct TALER_WireTransferIdentifierRawP wtid; + struct TALER_MasterSignatureP master_sig; + const char *err_name; + unsigned int err_line; + struct TALER_Amount amount; + struct GNUNET_TIME_Timestamp date; + const char *payto_uri; + const char *account_section; + struct DrainProfitsRequest *dpr; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("wtid", + &wtid), + TALER_JSON_spec_amount ("amount", + currency, + &amount), + GNUNET_JSON_spec_timestamp ("date", + &date), + GNUNET_JSON_spec_string ("account_section", + &account_section), + GNUNET_JSON_spec_string ("payto_uri", + &payto_uri), + GNUNET_JSON_spec_fixed_auto ("master_sig", + &master_sig), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (value, + spec, + &err_name, + &err_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid input to drain profits: %s#%u at %u (skipping)\n", + err_name, + err_line, + (unsigned int) idx); + json_dumpf (value, + stderr, + JSON_INDENT (2)); + global_ret = EXIT_FAILURE; + test_shutdown (); + return; + } + dpr = GNUNET_new (struct DrainProfitsRequest); + dpr->idx = idx; + dpr->h = + TALER_EXCHANGE_management_drain_profits (ctx, + exchange_url, + &wtid, + &amount, + date, + account_section, + payto_uri, + &master_sig, + &drain_profits_cb, + dpr); + GNUNET_CONTAINER_DLL_insert (dpr_head, + dpr_tail, + dpr); +} + + /** * Function called with information about the post upload keys operation result. * @@ -2098,6 +2265,10 @@ trigger_upload (const char *exchange_url) .key = OP_UPLOAD_SIGS, .cb = &upload_keys }, + { + .key = OP_DRAIN_PROFITS, + .cb = &upload_drain + }, { .key = OP_EXTENSIONS, .cb = &upload_extensions @@ -2787,6 +2958,112 @@ do_set_global_fee (char *const *args) } +/** + * Drain profits from exchange's escrow account to + * regular exchange account. + * + * @param args the array of command-line arguments to process next; + * args[0] must be the amount, + * args[1] must be the section of the escrow account to drain + * args[2] must be the payto://-URI of the target account + */ +static void +do_drain (char *const *args) +{ + struct TALER_WireTransferIdentifierRawP wtid; + struct GNUNET_TIME_Timestamp date; + struct TALER_Amount amount; + const char *account_section; + const char *payto_uri; + struct TALER_MasterSignatureP master_sig; + char *err; + + if (NULL != in) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Downloaded data was not consumed, refusing drain\n"); + test_shutdown (); + global_ret = EXIT_FAILURE; + return; + } + if ( (NULL == args[0]) || + (NULL == args[1]) || + (NULL == args[2]) || + (GNUNET_OK != + TALER_string_to_amount (args[0], + &amount)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Drain requires an amount, section name and target payto://-URI as arguments\n"); + test_shutdown (); + global_ret = EXIT_INVALIDARGUMENT; + return; + } + if ( (NULL == args[0]) || + (NULL == args[1]) || + (NULL == args[2]) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Drain requires an amount, section name and target payto://-URI as arguments\n"); + test_shutdown (); + global_ret = EXIT_INVALIDARGUMENT; + return; + } + if (GNUNET_OK != + TALER_string_to_amount (args[0], + &amount)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid amount `%s' specified for drain\n", + args[0]); + test_shutdown (); + global_ret = EXIT_INVALIDARGUMENT; + return; + } + account_section = args[1]; + payto_uri = args[2]; + err = TALER_payto_validate (payto_uri); + if (NULL != err) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Invalid payto://-URI `%s' specified for drain: %s\n", + payto_uri, + err); + GNUNET_free (err); + test_shutdown (); + global_ret = EXIT_INVALIDARGUMENT; + return; + } + if (GNUNET_OK != + load_offline_key (GNUNET_NO)) + return; + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &wtid, + sizeof (wtid)); + date = GNUNET_TIME_timestamp_get (); + TALER_exchange_offline_profit_drain_sign (&wtid, + date, + &amount, + account_section, + payto_uri, + &master_priv, + &master_sig); + output_operation (OP_DRAIN_PROFITS, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_data_auto ("wtid", + &wtid), + GNUNET_JSON_pack_string ("account_section", + account_section), + GNUNET_JSON_pack_string ("payto_uri", + payto_uri), + GNUNET_JSON_pack_timestamp ("date", + date), + GNUNET_JSON_pack_data_auto ("master_sig", + &master_sig))); + next (args + 3); +} + + /** * Function called with information about future keys. Dumps the JSON output * (on success), either into an internal buffer or to stdout (depending on @@ -4180,6 +4457,12 @@ work (void *cls) "sign global fees for the given year (year, history fee, kyc fee, account fee, purse fee, purse timeout, kyc timeout, history expiration and the maximum number of free purses per account must be given as arguments)", .cb = &do_set_global_fee }, + { + .name = "drain", + .help = + "drain profits from exchange escrow account to regular exchange operator account (amount, debit account configuration section and credit account payto://-URI must be given as arguments)", + .cb = &do_drain + }, { .name = "upload", .help = diff --git a/src/include/taler_exchange_service.h b/src/include/taler_exchange_service.h index 6c3ad7a01..e14f01ca2 100644 --- a/src/include/taler_exchange_service.h +++ b/src/include/taler_exchange_service.h @@ -3965,6 +3965,63 @@ TALER_EXCHANGE_management_post_extensions_cancel ( struct TALER_EXCHANGE_ManagementPostExtensionsHandle *ph); +/** + * Function called with information about the drain profits result. + * + * @param cls closure + * @param hr HTTP response data + */ +typedef void +(*TALER_EXCHANGE_ManagementDrainProfitsCallback) ( + void *cls, + const struct TALER_EXCHANGE_HttpResponse *hr); + + +/** + * @brief Handle for a POST /management/drain request. + */ +struct TALER_EXCHANGE_ManagementDrainProfitsHandle; + + +/** + * Uploads the drain profits request. + * + * @param ctx the context + * @param url HTTP base URL for the exchange + * @param wtid wire transfer identifier to use + * @param amount total to transfer + * @param date when was the request created + * @param account_section configuration section identifying account to debit + * @param payto_uri RFC 8905 URI of the account to credit + * @param master_sig signature affirming the operation + * @param cb function to call with the exchange's result + * @param cb_cls closure for @a cb + * @return the request handle; NULL upon error + */ +struct TALER_EXCHANGE_ManagementDrainProfitsHandle * +TALER_EXCHANGE_management_drain_profits ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Timestamp date, + const char *account_section, + const char *payto_uri, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementDrainProfitsCallback cb, + void *cb_cls); + + +/** + * Cancel #TALER_EXCHANGE_management_drain_profits() operation. + * + * @param dp handle of the operation to cancel + */ +void +TALER_EXCHANGE_management_drain_profits_cancel ( + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp); + + /** * Function called with information about the post revocation operation result. * diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 76157c43a..6adaac387 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -39,6 +39,7 @@ libtalerexchange_la_SOURCES = \ exchange_api_link.c \ exchange_api_management_auditor_disable.c \ exchange_api_management_auditor_enable.c \ + exchange_api_management_drain_profits.c \ exchange_api_management_get_keys.c \ exchange_api_management_post_keys.c \ exchange_api_management_post_extensions.c \ diff --git a/src/lib/exchange_api_management_drain_profits.c b/src/lib/exchange_api_management_drain_profits.c new file mode 100644 index 000000000..9cf1af85e --- /dev/null +++ b/src/lib/exchange_api_management_drain_profits.c @@ -0,0 +1,213 @@ +/* + This file is part of TALER + Copyright (C) 2020-2022 Taler Systems SA + + 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 + +*/ +/** + * @file lib/exchange_api_management_drain_profits.c + * @brief functions to set wire fees at an exchange + * @author Christian Grothoff + */ +#include "platform.h" +#include "taler_json_lib.h" +#include +#include "exchange_api_curl_defaults.h" +#include "taler_exchange_service.h" +#include "taler_signatures.h" +#include "taler_curl_lib.h" +#include "taler_json_lib.h" + + +struct TALER_EXCHANGE_ManagementDrainProfitsHandle +{ + + /** + * The url for this request. + */ + char *url; + + /** + * Minor context that holds body and headers. + */ + struct TALER_CURL_PostContext post_ctx; + + /** + * Handle for the request. + */ + struct GNUNET_CURL_Job *job; + + /** + * Function to call with the result. + */ + TALER_EXCHANGE_ManagementDrainProfitsCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Reference to the execution context. + */ + struct GNUNET_CURL_Context *ctx; +}; + + +/** + * Function called when we're done processing the + * HTTP /management/drain request. + * + * @param cls the `struct TALER_EXCHANGE_ManagementDrainProfitsHandle *` + * @param response_code HTTP response code, 0 on error + * @param response response body, NULL if not in JSON + */ +static void +handle_drain_profits_finished (void *cls, + long response_code, + const void *response) +{ + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp = cls; + const json_t *json = response; + struct TALER_EXCHANGE_HttpResponse hr = { + .http_status = (unsigned int) response_code, + .reply = json + }; + + dp->job = NULL; + switch (response_code) + { + case MHD_HTTP_NO_CONTENT: + break; + case MHD_HTTP_FORBIDDEN: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_CONFLICT: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + break; + case MHD_HTTP_PRECONDITION_FAILED: + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + break; + default: + /* unexpected response code */ + GNUNET_break_op (0); + hr.ec = TALER_JSON_get_error_code (json); + hr.hint = TALER_JSON_get_error_hint (json); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unexpected response code %u/%d for exchange management drain profits\n", + (unsigned int) response_code, + (int) hr.ec); + break; + } + if (NULL != dp->cb) + { + dp->cb (dp->cb_cls, + &hr); + dp->cb = NULL; + } + TALER_EXCHANGE_management_drain_profits_cancel (dp); +} + + +struct TALER_EXCHANGE_ManagementDrainProfitsHandle * +TALER_EXCHANGE_management_drain_profits ( + struct GNUNET_CURL_Context *ctx, + const char *url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_Amount *amount, + struct GNUNET_TIME_Timestamp date, + const char *account_section, + const char *payto_uri, + const struct TALER_MasterSignatureP *master_sig, + TALER_EXCHANGE_ManagementDrainProfitsCallback cb, + void *cb_cls) +{ + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp; + CURL *eh; + json_t *body; + + dp = GNUNET_new (struct TALER_EXCHANGE_ManagementDrainProfitsHandle); + dp->cb = cb; + dp->cb_cls = cb_cls; + dp->ctx = ctx; + dp->url = TALER_url_join (url, + "management/drain", + NULL); + if (NULL == dp->url) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not construct request URL.\n"); + GNUNET_free (dp); + return NULL; + } + body = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("debit_account_section", + account_section), + GNUNET_JSON_pack_string ("credit_payto_uri", + payto_uri), + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + GNUNET_JSON_pack_data_auto ("master_sig", + master_sig), + GNUNET_JSON_pack_timestamp ("date", + date), + TALER_JSON_pack_amount ("amount", + amount)); + eh = TALER_EXCHANGE_curl_easy_get_ (dp->url); + if ( (NULL == eh) || + (GNUNET_OK != + TALER_curl_easy_post (&dp->post_ctx, + eh, + body)) ) + { + GNUNET_break (0); + if (NULL != eh) + curl_easy_cleanup (eh); + json_decref (body); + GNUNET_free (dp->url); + return NULL; + } + json_decref (body); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Requesting URL '%s'\n", + dp->url); + dp->job = GNUNET_CURL_job_add2 (ctx, + eh, + dp->post_ctx.headers, + &handle_drain_profits_finished, + dp); + if (NULL == dp->job) + { + TALER_EXCHANGE_management_drain_profits_cancel (dp); + return NULL; + } + return dp; +} + + +void +TALER_EXCHANGE_management_drain_profits_cancel ( + struct TALER_EXCHANGE_ManagementDrainProfitsHandle *dp) +{ + if (NULL != dp->job) + { + GNUNET_CURL_job_cancel (dp->job); + dp->job = NULL; + } + TALER_curl_easy_post_finished (&dp->post_ctx); + GNUNET_free (dp->url); + GNUNET_free (dp); +}