merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 7192d66b632be5d1732fa121baa5f79483ad2721
parent b9f0772bb43ee9c15cc70bd145c8a5167d083a03
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Thu, 28 Aug 2025 07:05:01 +0200

try to fix #10194 (need to re-run test)

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 3++-
Msrc/backend/taler-merchant-httpd_exchanges.c | 20++++++++++++--------
Msrc/backend/taler-merchant-httpd_exchanges.h | 22++++++++++++++++++++--
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 17++++++++++-------
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c | 16++++++++++------
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/testing/test_kyc_api.conf | 3+++
Msrc/testing/test_merchant_api.conf | 6+++---
Msrc/testing/test_merchant_instance_auth.sh | 9+++++----
Msrc/testing/test_template.conf | 3+++
Msrc/util/currencies.conf | 10++++++++++
11 files changed, 139 insertions(+), 36 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1025,7 +1025,8 @@ full_url_track_callback (void *cls, * @param authn_s the value of the authorization header */ static void -process_basic_auth (struct TMH_HandlerContext *hc, const char*authn_s) +process_basic_auth (struct TMH_HandlerContext *hc, + const char *authn_s) { /* Handle token endpoint slightly differently: Only allow * instance password (Basic auth) to retrieve access token. diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -680,7 +680,7 @@ TMH_EXCHANGES_lookup_wire_fee ( } -enum GNUNET_GenericReturnValue +enum TMH_ExchangeStatus TMH_exchange_check_debit ( const char *instance_id, const struct TMH_Exchange *exchange, @@ -690,9 +690,13 @@ TMH_exchange_check_debit ( const struct TALER_EXCHANGE_Keys *keys = exchange->keys; bool have_kyc = false; bool no_access_token = true; + enum TMH_ExchangeStatus retry_ok = 0; + + if (GNUNET_TIME_absolute_is_past (exchange->first_retry)) + retry_ok = TMH_ES_RETRY_OK; if (NULL == keys) - return GNUNET_SYSERR; + return TMH_ES_NO_KEYS | retry_ok; if (0 != strcasecmp (keys->currency, max_amount->currency)) { @@ -701,7 +705,7 @@ TMH_exchange_check_debit ( exchange->url, keys->currency, max_amount->currency); - return GNUNET_SYSERR; + return TMH_ES_NO_CURR | retry_ok; } { struct TALER_NormalizedPayto np; @@ -713,10 +717,10 @@ TMH_exchange_check_debit ( np); GNUNET_free (np.normalized_payto); if (! account_ok) - return GNUNET_NO; + return TMH_ES_NO_ACC | retry_ok; } if (! keys->kyc_enabled) - return GNUNET_YES; + return TMH_ES_OK | retry_ok; { json_t *jlimits = NULL; @@ -796,7 +800,7 @@ TMH_exchange_check_debit ( TALER_amount_min (max_amount, max_amount, &kyc_limit); - return GNUNET_YES; + return TMH_ES_OK | retry_ok; } /* END of if qs > 0, NULL != jlimits */ } @@ -819,7 +823,7 @@ TMH_exchange_check_debit ( TALER_amount_set_zero ( max_amount->currency, max_amount)); - return GNUNET_YES; + return TMH_ES_OK | retry_ok; } /* In any case, abide by hard limits (unless we have custom rules). */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -846,7 +850,7 @@ TMH_exchange_check_debit ( TALER_amount_set_zero (max_amount->currency, max_amount)); } - return GNUNET_YES; + return TMH_ES_OK | retry_ok; } diff --git a/src/backend/taler-merchant-httpd_exchanges.h b/src/backend/taler-merchant-httpd_exchanges.h @@ -210,9 +210,27 @@ TMH_EXCHANGES_lookup_wire_fee ( * exchange; input is an existing maximum, that * can be lowered by this function due to transaction * limits and deposit limits of the exchange - * @return #GNUNET_OK if such a debit can happen + * @return #TMH_ES_OK if such a debit can happen + * #TMH_ES_NO_ACC if the exchange cannot + * be used due to account restrictions + * #TMH_ES_NO_CURR if the exchange cannot be used + * because it is for a different currency + * #TMH_ES_NO_KEYS if the exchange cannot be used + * because we do not even know its keys + * #TMH_ES_RETRY_OK if the exchange keys request + * could be retried (OR-bit!) */ -enum GNUNET_GenericReturnValue +enum TMH_ExchangeStatus +{ + TMH_ES_OK = 0, + TMH_ES_NO_ACC = 1, + TMH_ES_NO_CURR = 2, + TMH_ES_NO_KEYS = 3, + TMH_ES_RETRY_OK = 16, + TMH_ES_NO_ACC_RETRY_OK = TMH_ES_NO_ACC | TMH_ES_RETRY_OK, + TMH_ES_NO_CURR_RETRY_OK = TMH_ES_NO_CURR | TMH_ES_RETRY_OK, + TMH_ES_NO_KEYS_RETRY_OK = TMH_ES_NO_KEYS | TMH_ES_RETRY_OK, +} TMH_exchange_check_debit ( const char *instance_id, const struct TMH_Exchange *exchange, diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -1310,6 +1310,7 @@ process_pay_with_keys ( struct TMH_HandlerContext *hc = pc->hc; unsigned int group_size; struct TALER_Amount max_amount; + enum TMH_ExchangeStatus es; eg->fo = NULL; pc->batch_deposits.pending_at_eg--; @@ -1340,14 +1341,16 @@ process_pay_with_keys ( } max_amount = eg->total; - if (GNUNET_OK != - TMH_exchange_check_debit ( - pc->hc->instance->settings.id, - exchange, - pc->check_contract.wm, - &max_amount)) + es = TMH_exchange_check_debit ( + pc->hc->instance->settings.id, + exchange, + pc->check_contract.wm, + &max_amount); + if ( (TMH_ES_OK != es) && + (TMH_ES_RETRY_OK != es) ) { - if (eg->tried_force_keys) + if (eg->tried_force_keys || + (0 == (TMH_ES_RETRY_OK & es)) ) { GNUNET_break_op (0); resume_pay_with_error ( diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c @@ -42,9 +42,10 @@ add_token (void *cls, { json_t *pa = cls; bool refreshable; - const char*as; + const char *as; - as = TMH_get_name_by_scope (scope, &refreshable); + as = TMH_get_name_by_scope (scope, + &refreshable); if (NULL == as) { GNUNET_break (0); @@ -76,13 +77,16 @@ TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, { json_t *ta; enum GNUNET_DB_QueryStatus qs; - int64_t limit; - uint64_t offset = 0; + int64_t limit = -20; /* default */ + uint64_t offset; - limit = -20; /* default */ TALER_MHD_parse_request_snumber (connection, "limit", &limit); + if (limit > 0) + offset = 0; + else + offset = INT64_MAX; TALER_MHD_parse_request_number (connection, "offset", &offset); @@ -110,4 +114,4 @@ TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, } -/* end of taler-merchant-httpd_private-get-products.c */ +/* end of taler-merchant-httpd_private-get-instances-ID-tokens.c */ diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -550,6 +550,17 @@ struct OrderContext bool forced_reload; /** + * Did we find a working exchange? + */ + bool exchange_ok; + + /** + * Did we find an exchange that justifies + * reloading keys? + */ + bool promising_exchange; + + /** * Set to true once we have attempted to load exchanges * for the first time. */ @@ -2562,8 +2573,10 @@ phase_select_wire_method (struct OrderContext *oc) } if ( (want_choices > max_choices) && + (oc->set_exchanges.promising_exchange) && (! oc->set_exchanges.forced_reload) ) { + oc->set_exchanges.exchange_ok = false; /* Not all choices in the contract can work with these exchanges, try again with forcing /keys download */ for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; @@ -2792,7 +2805,7 @@ get_acceptable (struct OrderContext *oc, const struct TALER_Amount *max_needed = NULL; unsigned int priority = 42; /* make compiler happy */ json_t *j_exchange; - enum GNUNET_GenericReturnValue res; + enum TMH_ExchangeStatus res; struct TALER_Amount max_amount; for (unsigned int i = 0; i<oc->add_payment_details.num_max_choice_limits; i++) @@ -2842,21 +2855,57 @@ get_acceptable (struct OrderContext *oc, } switch (res) { - case GNUNET_OK: + case TMH_ES_OK: + case TMH_ES_RETRY_OK: priority = 1024; /* high */ + oc->set_exchanges.exchange_ok = true; break; - case GNUNET_NO: + case TMH_ES_NO_ACC: if (oc->set_exchanges.forced_reload) priority = 0; /* fresh negative response */ else priority = 512; /* stale negative response */ break; - case GNUNET_SYSERR: + case TMH_ES_NO_CURR: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case TMH_ES_NO_KEYS: if (oc->set_exchanges.forced_reload) priority = 256; /* fresh, no accounts yet */ else priority = 768; /* stale, no accounts yet */ break; + case TMH_ES_NO_ACC_RETRY_OK: + if (oc->set_exchanges.forced_reload) + { + priority = 0; /* fresh negative response */ + } + else + { + oc->set_exchanges.promising_exchange = true; + priority = 512; /* stale negative response */ + } + break; + case TMH_ES_NO_CURR_RETRY_OK: + if (oc->set_exchanges.forced_reload) + priority = 0; /* fresh negative response */ + else + priority = 512; /* stale negative response */ + break; + case TMH_ES_NO_KEYS_RETRY_OK: + if (oc->set_exchanges.forced_reload) + { + priority = 256; /* fresh, no accounts yet */ + } + else + { + oc->set_exchanges.promising_exchange = true; + priority = 768; /* stale, no accounts yet */ + } + break; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is %s, adding it!\n", @@ -2915,6 +2964,7 @@ keys_cb ( GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to download %skeys\n", rx->url); + oc->set_exchanges.promising_exchange = true; goto cleanup; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -3051,8 +3101,14 @@ phase_set_exchanges (struct OrderContext *oc) TMH_exchange_get_trusted (&get_exchange_keys, oc); } - else if (! oc->set_exchanges.forced_reload) + else if ( (! oc->set_exchanges.forced_reload) && + (oc->set_exchanges.promising_exchange) && + (! oc->set_exchanges.exchange_ok) ) { + for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head; + NULL != wmc; + wmc = wmc->next) + json_array_clear (wmc->exchanges); /* Try one more time with forcing /keys download */ oc->set_exchanges.forced_reload = true; TMH_exchange_get_trusted (&get_exchange_keys, diff --git a/src/testing/test_kyc_api.conf b/src/testing/test_kyc_api.conf @@ -3,6 +3,9 @@ [PATHS] TALER_TEST_HOME = test_merchant_api_home/ +[merchant-exchange-chf] +DISABLED = YES + [merchant-exchange-kudos] DISABLED = YES diff --git a/src/testing/test_merchant_api.conf b/src/testing/test_merchant_api.conf @@ -4,9 +4,6 @@ TALER_TEST_HOME = test_merchant_api_home/ DONAU_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/donau-system-runtime/ -[merchant-exchange-kudos] -DISABLED = YES - [taler-helper-crypto-rsa] LOOKAHEAD_SIGN = 10 days @@ -20,6 +17,9 @@ HTTP_PORT = 8082 [merchant-exchange-chf] DISABLED = YES +[merchant-exchange-kudos] +DISABLED = YES + [libeufin-bank] CURRENCY = EUR WIRE_TYPE = x-taler-bank diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -408,19 +408,20 @@ RTOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") echo " OK" >&2 -echo "Getting 2 login tokens with offset 2." >&2 +echo "Getting last 2 login tokens." >&2 STATUS=$(curl -H "Content-Type: application/json" \ -H "Authorization: Bearer $RWTOKEN" \ - 'http://localhost:9966/instances/second/private/tokens?limit=2&offset=4' \ + 'http://localhost:9966/instances/second/private/tokens?limit=-2' \ -w "%{http_code}" -s -o $LAST_RESPONSE) if [ "$STATUS" != "200" ] then + jq < "$LAST_RESPONSE" >&2 exit_fail "Expected 200 OK. Got: $STATUS" fi -TOKEN_SERIAL=$(jq -e -r .tokens[1].serial < "$LAST_RESPONSE") +TOKEN_SERIAL=$(jq -e -r .tokens[0].serial < "$LAST_RESPONSE") echo -n "Deleting second login token by serial..." >&2 @@ -436,7 +437,7 @@ then fi echo " OK" >&2 -echo -n "Using deleted login token..." >&2 +echo -n "Using deleted login token $RTOKEN..." >&2 STATUS=$(curl "http://localhost:9966/instances/second/private/orders" \ -H 'Authorization: Bearer '"$RTOKEN" \ diff --git a/src/testing/test_template.conf b/src/testing/test_template.conf @@ -4,6 +4,9 @@ TALER_TEST_HOME = test_merchant_api_home/ [merchant-exchange-kudos] DISABLED = YES +[merchant-exchange-chf] +DISABLED = YES + [exchange] CURRENCY = TESTKUDOS CURRENCY_ROUND_UNIT = TESTKUDOS:0.01 diff --git a/src/util/currencies.conf b/src/util/currencies.conf @@ -2,6 +2,7 @@ ENABLED = YES name = "Euro" code = "EUR" +common_amounts = "EUR:5" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 @@ -11,6 +12,7 @@ alt_unit_names = {"0":"€"} ENABLED = YES name = "Swiss Francs" code = "CHF" +common_amounts = "CHF:5" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 @@ -20,6 +22,7 @@ alt_unit_names = {"0":"Fr.","-2":"Rp."} ENABLED = NO name = "Hungarian Forint" code = "HUF" +common_amounts = "HUF:500" fractional_input_digits = 0 fractional_normal_digits = 0 fractional_trailing_zero_digits = 0 @@ -29,6 +32,7 @@ alt_unit_names = {"0":"Ft"} ENABLED = NO name = "US Dollar" code = "USD" +common_amounts = "USD:5" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 @@ -38,6 +42,7 @@ alt_unit_names = {"0":"$"} ENABLED = YES name = "Kudos (Taler Demonstrator)" code = "KUDOS" +common_amounts = "KUDOS:5" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 @@ -47,6 +52,7 @@ alt_unit_names = {"0":"ク"} ENABLED = YES name = "Test-kudos (Taler Demonstrator)" code = "TESTKUDOS" +common_amounts = "TESTKUDOS:5" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 @@ -56,6 +62,7 @@ alt_unit_names = {"0":"テ","3":"kテ","-3":"mテ"} ENABLED = NO name = "Japanese Yen" code = "JPY" +common_amounts = "JPY:1000" fractional_input_digits = 2 fractional_normal_digits = 0 fractional_trailing_zero_digits = 2 @@ -65,6 +72,7 @@ alt_unit_names = {"0":"¥"} ENABLED = NO name = "Bitcoin (Mainnet)" code = "BITCOINBTC" +common_amounts = "BITCOINBTC:0.001" fractional_input_digits = 8 fractional_normal_digits = 3 fractional_trailing_zero_digits = 0 @@ -74,6 +82,7 @@ alt_unit_names = {"0":"BTC","-3":"mBTC"} ENABLED = NO name = "WAI-ETHER (Ethereum)" code = "EthereumWAI" +common_amounts = "EthereumWAI:1000000" fractional_input_digits = 0 fractional_normal_digits = 0 fractional_trailing_zero_digits = 0 @@ -83,6 +92,7 @@ alt_unit_names = {"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":" ENABLED=YES name=NetzBon code=NETZBON +common_amounts = "NETZBON:5" fractional_input_digits=2 fractional_normal_digits=2 fractional_trailing_zero_digits=2