commit 1f8c8ce04a552a6352cb8074d769a9a70e8fadb2
parent d71302673998390b23a53e457637712dcf7a6088
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 10:59:44 +0200
add multi-currency support
Diffstat:
50 files changed, 2360 insertions(+), 477 deletions(-)
diff --git a/contrib/ci/jobs/2-test/3-startdb.sh b/contrib/ci/jobs/2-test/3-startdb.sh
@@ -12,9 +12,16 @@ sudo -u postgres "${PG_BIN}/pg_ctl" \
sleep 10
sudo -u postgres createuser -p "$PGPORT" root -s -w
-# 'talercheck' is used by the exchange, the merchant and libeufin-bank
-# that taler-unified-setup.sh brings up for the reducer and IBAN tests.
-sudo -u postgres createdb -p "$PGPORT" -O root talercheck
+# 'anastasistalercheck' is used by the exchange, the merchant and
+# libeufin-bank that taler-unified-setup.sh brings up for the reducer and
+# IBAN tests. taler-unified-setup.sh runs every one of those dbinits with
+# --reset, so the name must NOT be shared with another project's test
+# suite: the plain 'talercheck' that the exchange, merchant, sync and donau
+# suites all use gets its schema dropped out from under a running exchange
+# whenever two of those suites overlap on one host, which shows up here as
+# spurious 500s ('relation "denominations" does not exist') from
+# taler-exchange-offline.
+sudo -u postgres createdb -p "$PGPORT" -O root anastasistalercheck
# The provider database: 'anastasischeck' for the stasis and testing
# suites, 'anastasischeck1' .. 'anastasischeck4' for the four providers
# the reducer/CLI tests run against (see src/cli/test_anastasis_reducer_*.conf).
diff --git a/contrib/redux.countries.json b/contrib/redux.countries.json
@@ -6,91 +6,106 @@
"code" : "al",
"name" : "Albania",
"continent" : "Europe",
- "call_code" : "+355"
+ "call_code" : "+355",
+ "currency" : "ALL"
},
{
"code" : "be",
"name" : "Belgium",
"continent" : "Europe",
- "call_code" : "+32"
+ "call_code" : "+32",
+ "currency" : "EUR"
},
{
"code" : "ch",
"name" : "Switzerland",
"continent" : "Europe",
- "call_code" : "+41"
+ "call_code" : "+41",
+ "currency" : "CHF"
},
{
"code" : "cz",
"name" : "Czech Republic",
"continent" : "Europe",
- "call_code" : "+420"
+ "call_code" : "+420",
+ "currency" : "CZK"
},
{
"code" : "de",
"name" : "Germany",
"continent" : "Europe",
- "call_code" : "+49"
+ "call_code" : "+49",
+ "currency" : "EUR"
},
{
"code" : "dk",
"name" : "Denmark",
"continent" : "Europe",
- "call_code" : "+45"
+ "call_code" : "+45",
+ "currency" : "DKK"
},
{
"code" : "es",
"name" : "Spain",
"continent" : "Europe",
- "call_code" : "+44"
+ "call_code" : "+44",
+ "currency" : "EUR"
},
{
"code" : "fr",
"name" : "France",
"continent" : "Europe",
- "call_code" : "+33"
+ "call_code" : "+33",
+ "currency" : "EUR"
},
{
"code" : "in",
"name" : "India",
"continent" : "India",
- "call_code" : "+91"
+ "call_code" : "+91",
+ "currency" : "INR"
},
{
"code" : "it",
"name" : "Italy",
"continent" : "Europe",
- "call_code" : "+39"
+ "call_code" : "+39",
+ "currency" : "EUR"
},
{
"code" : "jp",
"name" : "Japan",
"continent" : "Asia",
- "call_code" : "+81"
+ "call_code" : "+81",
+ "currency" : "JPY"
},
{
"code" : "nl",
"name" : "Netherlands",
"continent" : "Europe",
- "call_code" : "+31"
+ "call_code" : "+31",
+ "currency" : "EUR"
},
{
"code" : "sk",
"name" : "Slovakia",
"continent" : "Europe",
- "call_code" : "+421"
+ "call_code" : "+421",
+ "currency" : "EUR"
},
{
"code" : "us",
"name" : "United States of America (USA)",
"continent" : "North America",
- "call_code" : "+1"
+ "call_code" : "+1",
+ "currency" : "USD"
},
{
"code" : "xx",
"name" : "Testland",
"continent" : "Demoworld",
- "call_code" : "+0"
+ "call_code" : "+0",
+ "currency" : "TESTKUDOS"
}
]
}
diff --git a/doc/anastasis.conf.5 b/doc/anastasis.conf.5
@@ -64,6 +64,13 @@ Values that represent an amount are in the usual amount syntax:
\fBCURRENCY:VALUE.FRACTION\fP, e.g. \fBEUR:1.50\fP\&.
The \fBFRACTION\fP portion may extend up to 8 places.
.sp
+Values that represent a price may give one amount per currency, separated
+by ";", e.g. \fBEUR:1.50;CHF:1.50;USD:1.80\fP\&. Within one such option
+either every amount is zero (the option is free) or none is: charging in
+one currency and not in another does not price the service, it prices the
+user\(aqs choice of currency, and \fBanastasis\-httpd\fP refuses to start on
+it. An empty value means the option is free.
+.sp
Files containing default values for many of the options described below
are installed under \fB$ANASTASIS_PREFIX/share/taler/config.d/\fP\&.
The configuration file given with \fB\-c\fP to Anastasis binaries
@@ -79,11 +86,24 @@ The following options are from the \fB[anastasis]\fP section and used by
the \fBanastasis\-httpd\fP service.
.INDENT 0.0
.TP
+.B CURRENCIES
+Currencies the service is priced in, separated by ";", i.e.
+"EUR;CHF;USD". The first entry is the primary currency, which is what
+the scalar fields of the \fB/config\fP response report for the benefit of
+clients that predate multi\-currency support. Every priced option must
+give a price in exactly these currencies, or \fBanastasis\-httpd\fP refuses
+to start: without that check a user could discover during recovery that
+some step is unavailable in the currency they hold, which is the worst
+possible moment. May be omitted by a provider pricing in a single
+currency, in which case it is derived from the fees themselves.
+.TP
.B ANNUAL_FEE
-Annual fee to be paid for policy uploads, i.e. "EUR:1.5".
+Annual fee to be paid for policy uploads, one amount per currency in
+\fBCURRENCIES\fP, i.e. "EUR:1.5;CHF:1.5;USD:1.8".
.TP
.B TRUTH_UPLOAD_FEE
-Annual fee to be paid for truth uploads, i.e. "EUR:1.5".
+Annual fee to be paid for truth uploads, one amount per currency, i.e.
+"EUR:1.5;CHF:1.5;USD:1.8".
.TP
.B DB
Database backend to use, only \fBpostgres\fP is supported right now.
diff --git a/doc/sphinx/reducer.rst b/doc/sphinx/reducer.rst
@@ -1057,9 +1057,51 @@ The reducer will simply transition to the ``SECRET_EDITING`` state:
}
Here, ``upload_fees`` is an array of applicable upload fees for the
-given policy expiration time. This is an array because fees could
-be in different currencies. The final cost may be lower if the
-user already paid for some of the time.
+given policy expiration time. It is advisory: the orders the providers
+create carry *every* currency they accept as a payable choice, and the
+wallet is free to settle in one of the others.
+
+Each provider contributes exactly one amount to this total --- in
+``preferred_currency`` if it accepts that currency, and otherwise in its
+own primary currency. It is therefore an array, but it collapses to a
+single entry whenever every provider involved accepts the preferred
+currency. More than one entry means the backup will be settled as more
+than one order in more than one currency, which is accurate: each
+provider is a separate order at a separate merchant backend.
+
+The state also carries ``currencies``, the currencies that *all* of the
+providers used by this backup accept, i.e. the ones in which the whole
+backup can be paid for with a single choice of currency. It may be
+empty, which is not an error.
+
+The final cost may be lower if the user already paid for some of the
+time.
+
+
+**select_currency:**
+
+Selects the currency the user would rather pay in. Valid wherever
+``select_country`` is, and also while user attributes are being
+collected.
+
+Arguments (example):
+
+.. code-block:: json
+
+ {
+ "currency": "CHF"
+ }
+
+The choice is advisory, as described above for ``upload_fees``: it
+decides which currency totals are quoted in and which one a user
+interface puts in front of the user, not what the wallet must pay in.
+It is not checked against the providers, because when the currency is
+usually chosen their ``/config`` requests are still in flight, and a
+currency that no provider accepts is not an error either --- it just
+means no single-currency total can be quoted.
+
+If it is never set, it defaults to the ``currency`` of the selected
+country.
If the array of ``policies`` is currently empty, the reducer will
return an error response instead of allowing the user to continue.
diff --git a/doc/sphinx/rest.rst b/doc/sphinx/rest.rst
@@ -436,17 +436,40 @@ Receiving Configuration
// Maximum policy upload size supported.
storage_limit_in_megabytes: number;
- // Payment required to maintain an account to store policy documents for a year.
+ // Currencies the provider prices its service in, primary currency
+ // first. Orders created by the provider offer every one of these
+ // as a payable choice, so the wallet decides which one is used.
+ // @since protocol **v3**.
+ currencies?: string[];
+
+ // Payment required to maintain an account to store policy documents for a year,
+ // in the provider's *primary* currency.
// Users can pay more, in which case the storage time will go up proportionally.
annual_fee: Amount;
- // Payment required to upload truth. To be paid per upload.
+ // Payment required to maintain an account for a year, one entry per
+ // currency in ``currencies``. Empty if the service is free.
+ // Either every entry is zero or none is: a provider that would charge
+ // in one currency and not in another is refusing to price its service
+ // and is rejected by its own configuration check.
+ // @since protocol **v3**.
+ annual_fees?: Amount[];
+
+ // Payment required to upload truth, in the primary currency. To be paid per upload.
truth_upload_fee: Amount;
+ // Payment required to upload truth, one entry per currency.
+ // @since protocol **v3**.
+ truth_upload_fees?: Amount[];
+
// Limit on the liability that the provider is offering with
- // respect to the services provided.
+ // respect to the services provided, in the primary currency.
liability_limit: Amount;
+ // Limit on the liability, one entry per currency.
+ // @since protocol **v3**.
+ liability_limits?: Amount[];
+
// Salt value with 128 bits of entropy.
// Different providers
// will use different high-entropy salt values. The resulting
@@ -464,9 +487,17 @@ Receiving Configuration
// Name of the authorization method.
type: string;
- // Fee for accessing key share using this method.
+ // Fee for accessing key share using this method, in the provider's
+ // primary currency.
cost: Amount;
+ // Fee for accessing key share using this method, one entry per
+ // currency in ``currencies``. Empty means the method is free,
+ // which is what an IBAN-style method wants when the wire transfer
+ // the user has to make is already the cost of using it.
+ // @since protocol **v3**.
+ costs?: Amount[];
+
}
.. _terms:
diff --git a/src/authorization/anastasis_authorization_plugin.c b/src/authorization/anastasis_authorization_plugin.c
@@ -84,7 +84,7 @@ ANASTASIS_authorization_plugin_load (
char *sec_name;
struct AuthPlugin *ap;
char *currency;
- struct TALER_Amount cost;
+ struct TALER_AmountList costs;
for (ap = ap_head; NULL != ap; ap = ap->next)
if (0 == strcmp (method,
@@ -103,11 +103,14 @@ ANASTASIS_authorization_plugin_load (
}
ap = GNUNET_new (struct AuthPlugin);
ap->ac.cfg = AH_cfg;
+ /* The list has already been validated at startup by
+ anastasis-httpd's parse_prices(), so anything wrong here means the
+ configuration changed underneath us. */
if (GNUNET_OK !=
- TALER_config_get_amount (AH_cfg,
- sec_name,
- "COST",
- &cost))
+ TALER_config_get_amount_list (AH_cfg,
+ sec_name,
+ "COST",
+ &costs))
{
GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
sec_name,
@@ -132,10 +135,11 @@ ANASTASIS_authorization_plugin_load (
"Authentication method `%s' not supported\n",
method);
GNUNET_free (lib_name);
+ TALER_amount_list_free (&costs);
GNUNET_free (ap);
return NULL;
}
- authorization->cost = cost;
+ authorization->costs = costs;
ap->name = GNUNET_strdup (method);
ap->lib_name = lib_name;
ap->authorization = authorization;
@@ -156,6 +160,7 @@ ANASTASIS_authorization_plugin_shutdown (void)
GNUNET_CONTAINER_DLL_remove (ap_head,
ap_tail,
ap);
+ TALER_amount_list_free (&ap->authorization->costs);
GNUNET_PLUGIN_unload (ap->lib_name,
ap->authorization);
GNUNET_free (ap->lib_name);
diff --git a/src/backend/anastasis-httpd.c b/src/backend/anastasis-httpd.c
@@ -30,6 +30,7 @@
#include "anastasis-httpd_truth.h"
#include "anastasis-httpd_terms.h"
#include "anastasis-httpd_config.h"
+#include <taler/taler_json_lib.h>
/**
@@ -38,24 +39,34 @@
unsigned long long int AH_upload_limit_mb;
/**
- * Annual fee for the backup account.
+ * Currencies this provider prices its service in, primary currency first.
*/
-struct TALER_Amount AH_annual_fee;
+char **AH_currencies;
/**
- * Fee for a truth upload.
+ * Length of the #AH_currencies array.
*/
-struct TALER_Amount AH_truth_upload_fee;
+unsigned int AH_currencies_len;
/**
- * Amount of insurance.
+ * Annual fee for the backup account, per currency.
*/
-struct TALER_Amount AH_insurance;
+struct TALER_AmountList AH_annual_fees;
/**
- * Cost for secure question truth download.
+ * Fee for a truth upload, per currency.
*/
-struct TALER_Amount AH_question_cost;
+struct TALER_AmountList AH_truth_upload_fees;
+
+/**
+ * Amount of insurance, per currency.
+ */
+struct TALER_AmountList AH_insurance;
+
+/**
+ * Cost for secure question truth download, per currency.
+ */
+struct TALER_AmountList AH_question_costs;
/**
* Our configuration.
@@ -483,6 +494,408 @@ do_shutdown (void *cls)
GNUNET_CONTAINER_heap_destroy (AH_to_heap);
AH_to_heap = NULL;
}
+ TALER_amount_list_free (&AH_annual_fees);
+ TALER_amount_list_free (&AH_truth_upload_fees);
+ TALER_amount_list_free (&AH_insurance);
+ TALER_amount_list_free (&AH_question_costs);
+ for (unsigned int i = 0; i<AH_currencies_len; i++)
+ GNUNET_free (AH_currencies[i]);
+ GNUNET_array_grow (AH_currencies,
+ AH_currencies_len,
+ 0);
+}
+
+
+json_t *
+AH_make_order (const char *order_id,
+ const char *summary,
+ const struct TALER_AmountList *prices)
+{
+ json_t *choices;
+ json_t *order;
+
+ GNUNET_assert (0 != prices->tal_len);
+ choices = json_array ();
+ GNUNET_assert (NULL != choices);
+ for (unsigned int i = 0; i<prices->tal_len; i++)
+ {
+ json_t *choice;
+
+ GNUNET_assert (! TALER_amount_is_zero (&prices->tal[i]));
+ choice = GNUNET_JSON_PACK (
+ TALER_JSON_pack_amount ("amount",
+ &prices->tal[i]),
+ GNUNET_JSON_pack_string ("description",
+ summary));
+ GNUNET_assert (0 ==
+ json_array_append_new (choices,
+ choice));
+ }
+ order = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_uint64 ("version",
+ 1),
+ GNUNET_JSON_pack_string ("order_id",
+ order_id),
+ GNUNET_JSON_pack_string ("summary",
+ summary),
+ GNUNET_JSON_pack_array_steal ("choices",
+ choices));
+ return order;
+}
+
+
+enum GNUNET_GenericReturnValue
+AH_paid_amount (const struct TALER_MERCHANT_GetPrivateOrderResponse *osr,
+ struct TALER_Amount *amount)
+{
+ const json_t *contract = osr->details.ok.details.paid.contract_terms;
+ int ci = osr->details.ok.details.paid.choice_index;
+ const json_t *amount_obj;
+ struct GNUNET_JSON_Specification cspec[] = {
+ TALER_JSON_spec_amount_any ("amount",
+ amount),
+ GNUNET_JSON_spec_end ()
+ };
+
+ amount_obj = (0 > ci)
+ ? contract /* legacy "v0" order, made before the upgrade */
+ : json_array_get (json_object_get (contract,
+ "choices"),
+ ci);
+ if (NULL == amount_obj)
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_JSON_parse (amount_obj,
+ cspec,
+ NULL, NULL);
+}
+
+
+const struct TALER_Amount *
+AH_primary_price (const struct TALER_AmountList *al)
+{
+ static struct TALER_Amount zero;
+ const struct TALER_Amount *a;
+
+ GNUNET_assert (0 < AH_currencies_len);
+ a = TALER_amount_list_find (al,
+ AH_currencies[0]);
+ if (NULL != a)
+ return a;
+ /* The option is free. Report zero in the primary currency, which is
+ exactly what a single-currency provider running for free emitted
+ before there were price lists. */
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_set_zero (AH_currencies[0],
+ &zero));
+ return &zero;
+}
+
+
+enum GNUNET_GenericReturnValue
+AH_check_price (const struct TALER_AmountList *al,
+ const char *section,
+ const char *option)
+{
+ switch (TALER_amount_list_check_uniform (al))
+ {
+ case GNUNET_SYSERR:
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Price list error: option `%s' in section `%s' is `%s',"
+ " which prices some currencies at zero and others above"
+ " zero. Either all of them are zero (the option is free)"
+ " or none is.\n",
+ option,
+ section,
+ TALER_amount_list2s (al));
+ return GNUNET_SYSERR;
+ case GNUNET_NO:
+ /* Free is a first-class answer, and exempt from the coverage check
+ below: an operator must be able to say "free" without having to
+ enumerate every currency to do it. */
+ return GNUNET_NO;
+ case GNUNET_OK:
+ break;
+ }
+ if (! TALER_amount_list_covers (al,
+ (const char *const *) AH_currencies,
+ AH_currencies_len))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Price list error: option `%s' in section `%s' is `%s',"
+ " which does not price in exactly the currencies given in"
+ " [anastasis] CURRENCIES.\n",
+ option,
+ section,
+ TALER_amount_list2s (al));
+ /* Naming the missing currency is what makes this actionable: the
+ failure mode it guards against is an operator adding a currency
+ and overlooking one of the many COST options. */
+ for (unsigned int i = 0; i<AH_currencies_len; i++)
+ if (NULL ==
+ TALER_amount_list_find (al,
+ AH_currencies[i]))
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "No price given for currency `%s' in option `%s' of"
+ " section `%s'\n",
+ AH_currencies[i],
+ option,
+ section);
+ for (unsigned int i = 0; i<al->tal_len; i++)
+ {
+ bool found = false;
+
+ for (unsigned int j = 0; j<AH_currencies_len; j++)
+ if (0 == strcasecmp (al->tal[i].currency,
+ AH_currencies[j]))
+ found = true;
+ if (! found)
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Currency `%s' is priced in option `%s' of section"
+ " `%s' but is not in [anastasis] CURRENCIES\n",
+ al->tal[i].currency,
+ option,
+ section);
+ }
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Closure for #check_method_cost_cb().
+ */
+struct MethodCostContext
+{
+ /**
+ * Set to true if some enabled authorization method is misconfigured.
+ */
+ bool failure;
+};
+
+
+/**
+ * Validate the COST of one enabled authorization method.
+ *
+ * The plugins themselves are loaded lazily, on the first ``/config``
+ * request, so waiting for the loader to notice a bad COST would mean the
+ * provider starts and only fails once a client asks --- by which time the
+ * operator is no longer watching. The configuration file can be checked
+ * without loading anything, so it is checked here.
+ *
+ * @param cls a `struct MethodCostContext *`
+ * @param section configuration section to inspect
+ */
+static void
+check_method_cost_cb (void *cls,
+ const char *section)
+{
+ struct MethodCostContext *mcc = cls;
+ struct TALER_AmountList costs;
+
+ if (0 != strncasecmp (section,
+ "authorization-",
+ strlen ("authorization-")))
+ return;
+ if (GNUNET_YES !=
+ GNUNET_CONFIGURATION_get_value_yesno (AH_cfg,
+ section,
+ "ENABLED"))
+ return;
+ /* An absent COST is an error, not "free": a typo in the option name
+ must not silently give the method away. Free is spelled with an
+ empty value, or with a zero in every currency. */
+ if (GNUNET_OK !=
+ TALER_config_get_amount_list (AH_cfg,
+ section,
+ "COST",
+ &costs))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ section,
+ "COST");
+ mcc->failure = true;
+ return;
+ }
+ if (GNUNET_SYSERR ==
+ AH_check_price (&costs,
+ section,
+ "COST"))
+ mcc->failure = true;
+ TALER_amount_list_free (&costs);
+}
+
+
+/**
+ * Determine the currencies this provider prices its service in.
+ *
+ * Normally these are given explicitly in [anastasis] CURRENCIES, primary
+ * currency first. A configuration written before multi-currency support
+ * has no such option; for those, the single currency the provider was
+ * already using is derived from the first priced option, which keeps every
+ * existing configuration file working unchanged. Any disagreement between
+ * the options is then caught by the per-option coverage check, which can
+ * name the offending option.
+ *
+ * @param cfg configuration to use
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_currencies (const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ char *str;
+
+ if (GNUNET_OK !=
+ GNUNET_CONFIGURATION_get_value_string (cfg,
+ "anastasis",
+ "CURRENCIES",
+ &str))
+ {
+ const struct TALER_AmountList *als[] = {
+ &AH_annual_fees,
+ &AH_truth_upload_fees,
+ &AH_insurance,
+ &AH_question_costs
+ };
+
+ /* A priced option names the currency authoritatively; failing that,
+ an option that is free but written out as e.g. "EUR:0" still names
+ one, which is how a provider running entirely for free is
+ configured today. */
+ for (unsigned int pass = 0; pass < 2; pass++)
+ for (unsigned int i = 0; i<sizeof (als) / sizeof (als[0]); i++)
+ {
+ if ( (0 == pass) &&
+ (GNUNET_OK !=
+ TALER_amount_list_check_uniform (als[i])) )
+ continue;
+ if (0 == als[i]->tal_len)
+ continue;
+ if (1 != als[i]->tal_len)
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "CURRENCIES");
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "A provider pricing in more than one currency must say so explicitly.\n");
+ return GNUNET_SYSERR;
+ }
+ GNUNET_array_append (AH_currencies,
+ AH_currencies_len,
+ GNUNET_strdup (als[i]->tal[0].currency));
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "[anastasis] CURRENCIES not configured, assuming `%s'\n",
+ AH_currencies[0]);
+ return GNUNET_OK;
+ }
+ /* Not one option names a currency, so there is nothing to derive
+ from and the legacy scalar fields of /config would have no
+ currency to report either. */
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "CURRENCIES");
+ return GNUNET_SYSERR;
+ }
+ for (const char *tok = strtok (str, ";");
+ NULL != tok;
+ tok = strtok (NULL, ";"))
+ {
+ if (GNUNET_OK !=
+ TALER_check_currency (tok))
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "CURRENCIES",
+ tok);
+ GNUNET_free (str);
+ return GNUNET_SYSERR;
+ }
+ for (unsigned int i = 0; i<AH_currencies_len; i++)
+ if (0 == strcasecmp (tok,
+ AH_currencies[i]))
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "CURRENCIES",
+ "currency given more than once");
+ GNUNET_free (str);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_array_append (AH_currencies,
+ AH_currencies_len,
+ GNUNET_strdup (tok));
+ }
+ GNUNET_free (str);
+ if (0 == AH_currencies_len)
+ {
+ GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
+ "anastasis",
+ "CURRENCIES",
+ "no currency given");
+ return GNUNET_SYSERR;
+ }
+ return GNUNET_OK;
+}
+
+
+/**
+ * Parse and validate everything this provider charges for.
+ *
+ * @param cfg configuration to use
+ * @return #GNUNET_OK on success
+ */
+static enum GNUNET_GenericReturnValue
+parse_prices (const struct GNUNET_CONFIGURATION_Handle *cfg)
+{
+ static const struct
+ {
+ const char *section;
+ const char *option;
+ struct TALER_AmountList *al;
+ } prices[] = {
+ { "anastasis", "INSURANCE", &AH_insurance },
+ { "authorization-question", "COST", &AH_question_costs },
+ { "anastasis", "ANNUAL_FEE", &AH_annual_fees },
+ { "anastasis", "TRUTH_UPLOAD_FEE", &AH_truth_upload_fees }
+ };
+ struct MethodCostContext mcc = {
+ .failure = false
+ };
+
+ for (unsigned int i = 0; i<sizeof (prices) / sizeof (prices[0]); i++)
+ {
+ if (GNUNET_OK !=
+ TALER_config_get_amount_list (cfg,
+ prices[i].section,
+ prices[i].option,
+ prices[i].al))
+ {
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ prices[i].section,
+ prices[i].option);
+ return GNUNET_SYSERR;
+ }
+ }
+ /* The currency list has to exist before anything can be checked
+ against it, but deriving it from the prices needs them parsed. */
+ if (GNUNET_OK !=
+ parse_currencies (cfg))
+ return GNUNET_SYSERR;
+ for (unsigned int i = 0; i<sizeof (prices) / sizeof (prices[0]); i++)
+ if (GNUNET_SYSERR ==
+ AH_check_price (prices[i].al,
+ prices[i].section,
+ prices[i].option))
+ return GNUNET_SYSERR;
+ GNUNET_CONFIGURATION_iterate_sections (cfg,
+ &check_method_cost_cb,
+ &mcc);
+ if (mcc.failure)
+ return GNUNET_SYSERR;
+ return GNUNET_OK;
}
@@ -622,50 +1035,8 @@ run (void *cls,
return;
}
if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "anastasis",
- "INSURANCE",
- &AH_insurance))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "anastasis",
- "INSURANCE");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "authorization-question",
- "COST",
- &AH_question_cost))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "authorization-question",
- "COST");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "anastasis",
- "ANNUAL_FEE",
- &AH_annual_fee))
+ parse_prices (config))
{
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "anastasis",
- "ANNUAL_FEE");
- GNUNET_SCHEDULER_shutdown ();
- return;
- }
- if (GNUNET_OK !=
- TALER_config_get_amount (config,
- "anastasis",
- "TRUTH_UPLOAD_FEE",
- &AH_truth_upload_fee))
- {
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
- "anastasis",
- "TRUTH_UPLOAD_FEE");
GNUNET_SCHEDULER_shutdown ();
return;
}
diff --git a/src/backend/anastasis-httpd.h b/src/backend/anastasis-httpd.h
@@ -25,6 +25,7 @@
#include "anastasis_database_lib.h"
#include <microhttpd.h>
#include <taler/taler_mhd_lib.h>
+#include <taler/merchant/get-private-orders-ORDER_ID.h>
#include <gnunet/gnunet_mhd_compat.h>
@@ -145,24 +146,124 @@ struct TM_HandlerContext
extern unsigned long long AH_upload_limit_mb;
/**
- * Annual fee for the backup account.
+ * Currencies this provider prices its service in, primary currency
+ * first. From [anastasis] CURRENCIES. Every priced option below offers
+ * exactly these currencies; that is checked at startup.
*/
-extern struct TALER_Amount AH_annual_fee;
+extern char **AH_currencies;
/**
- * Fee for a truth upload.
+ * Length of the #AH_currencies array.
*/
-extern struct TALER_Amount AH_truth_upload_fee;
+extern unsigned int AH_currencies_len;
/**
- * Amount of insurance.
+ * Annual fee for the backup account, per currency.
*/
-extern struct TALER_Amount AH_insurance;
+extern struct TALER_AmountList AH_annual_fees;
/**
- * Cost for secure question truth download.
+ * Fee for a truth upload, per currency.
*/
-extern struct TALER_Amount AH_question_cost;
+extern struct TALER_AmountList AH_truth_upload_fees;
+
+/**
+ * Amount of insurance, per currency.
+ */
+extern struct TALER_AmountList AH_insurance;
+
+/**
+ * Cost for secure question truth download, per currency.
+ */
+extern struct TALER_AmountList AH_question_costs;
+
+
+/**
+ * Check that @a al is a usable price for a configuration option, and
+ * fail the process if it is not.
+ *
+ * A price must be uniformly free or uniformly priced: offering the same
+ * thing for money in one currency and for nothing in another does not
+ * price the service, it prices the user's choice of currency, and every
+ * wallet would simply pick the free one. A priced option must further
+ * offer exactly the currencies in #AH_currencies, or a user would find
+ * out that some step of recovery is unavailable in their currency only
+ * once the backup exists and the money is spent.
+ *
+ * @param al price list to check
+ * @param section configuration section it came from, for diagnostics
+ * @param option configuration option it came from, for diagnostics
+ * @return #GNUNET_OK if @a al is priced in all of #AH_currencies,
+ * #GNUNET_NO if @a al is free (empty or all zero),
+ * #GNUNET_SYSERR if @a al is unusable; the caller must shut down
+ */
+enum GNUNET_GenericReturnValue
+AH_check_price (const struct TALER_AmountList *al,
+ const char *section,
+ const char *option);
+
+
+/**
+ * Return the price of @a al in the primary currency, for the scalar
+ * fields that older clients read.
+ *
+ * A free option has no entry for any currency, and reports as zero in
+ * the primary currency --- which is what a single-currency provider
+ * running for free already emitted before there were price lists.
+ *
+ * @param al price list to look in
+ * @return price in the primary currency; the pointer is only valid
+ * until the next call
+ */
+const struct TALER_Amount *
+AH_primary_price (const struct TALER_AmountList *al);
+
+
+/**
+ * Build the skeleton of a merchant "v1" order that offers every currency
+ * in @a prices as a payable choice, letting the wallet settle in whichever
+ * one it holds.
+ *
+ * Every entry of @a prices becomes a choice: none is dropped and none can
+ * be zero, because #AH_check_price() refused to let the provider start on
+ * a price list that mixes free and non-free currencies. So the order is
+ * either complete and payable in every currency the provider advertises,
+ * or the caller decided the option was free and does not create an order
+ * at all.
+ *
+ * The caller adds whatever else the order needs (products, deadlines, …)
+ * to the returned object.
+ *
+ * @param order_id order ID to use
+ * @param summary human-readable summary of the order
+ * @param prices what the order costs, one entry per currency; must not
+ * be empty and must not contain a zero
+ * @return the order object, caller must `json_decref()` it
+ */
+json_t *
+AH_make_order (const char *order_id,
+ const char *summary,
+ const struct TALER_AmountList *prices);
+
+
+/**
+ * Extract from a paid order status the amount the wallet actually paid,
+ * in the currency it chose.
+ *
+ * A "v1" order has no top-level amount; the choice that was settled is at
+ * ``choices[choice_index]``. A ``choice_index`` of -1 means the contract
+ * is a legacy "v0" one with a single top-level amount --- which is what
+ * orders created before this provider was upgraded look like, and they
+ * still have to settle.
+ *
+ * @param osr order status to inspect, must be #TALER_MERCHANT_OSC_PAID
+ * @param[out] amount set to the amount that was paid
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR if the contract terms
+ * did not contain a usable amount
+ */
+enum GNUNET_GenericReturnValue
+AH_paid_amount (const struct TALER_MERCHANT_GetPrivateOrderResponse *osr,
+ struct TALER_Amount *amount);
/**
* Our Taler backend to process payments.
diff --git a/src/backend/anastasis-httpd_config.c b/src/backend/anastasis-httpd_config.c
@@ -65,7 +65,9 @@ add_methods (void *cls,
GNUNET_JSON_pack_string ("type",
section),
TALER_JSON_pack_amount ("cost",
- &p->cost));
+ AH_primary_price (&p->costs)),
+ TALER_JSON_pack_amount_list ("costs",
+ &p->costs));
GNUNET_assert (
0 ==
json_array_append_new (method_arr,
@@ -82,8 +84,14 @@ AH_handler_config (struct AH_RequestHandler *rh,
if (NULL == response)
{
json_t *method_arr = json_array ();
+ json_t *currency_arr = json_array ();
GNUNET_assert (NULL != method_arr);
+ GNUNET_assert (NULL != currency_arr);
+ for (unsigned int i = 0; i<AH_currencies_len; i++)
+ GNUNET_assert (0 ==
+ json_array_append_new (currency_arr,
+ json_string (AH_currencies[i])));
{
json_t *method;
@@ -91,7 +99,9 @@ AH_handler_config (struct AH_RequestHandler *rh,
GNUNET_JSON_pack_string ("type",
"question"),
TALER_JSON_pack_amount ("cost",
- &AH_question_cost));
+ AH_primary_price (&AH_question_costs)),
+ TALER_JSON_pack_amount_list ("costs",
+ &AH_question_costs));
GNUNET_assert (
0 ==
json_array_append_new (method_arr,
@@ -106,9 +116,12 @@ AH_handler_config (struct AH_RequestHandler *rh,
"anastasis"),
/* Deliberately incompatible with 0:x:x: the key derivation was fixed to
use the full context string, so every key derived by an older client
- differs and backups made with one cannot be recovered by the other. */
+ differs and backups made with one cannot be recovered by the other.
+ 3 adds the plural "…s" fields and "currencies" without removing
+ anything, so the age stays at 1 and a version 2 client still works
+ against us, seeing only the primary currency. */
GNUNET_JSON_pack_string ("version",
- "2:0:1"),
+ "3:0:1"),
GNUNET_JSON_pack_string ("build_version",
PACKAGE_VERSION),
GNUNET_JSON_pack_string ("implementation",
@@ -119,12 +132,23 @@ AH_handler_config (struct AH_RequestHandler *rh,
method_arr),
GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes",
AH_upload_limit_mb),
+ GNUNET_JSON_pack_array_steal ("currencies",
+ currency_arr),
+ /* The scalar fields report the primary currency and stay for the
+ benefit of version 2 clients; the plural ones carry the full
+ list. */
TALER_JSON_pack_amount ("annual_fee",
- &AH_annual_fee),
+ AH_primary_price (&AH_annual_fees)),
+ TALER_JSON_pack_amount_list ("annual_fees",
+ &AH_annual_fees),
TALER_JSON_pack_amount ("truth_upload_fee",
- &AH_truth_upload_fee),
+ AH_primary_price (&AH_truth_upload_fees)),
+ TALER_JSON_pack_amount_list ("truth_upload_fees",
+ &AH_truth_upload_fees),
TALER_JSON_pack_amount ("liability_limit",
- &AH_insurance),
+ AH_primary_price (&AH_insurance)),
+ TALER_JSON_pack_amount_list ("liability_limits",
+ &AH_insurance),
GNUNET_JSON_pack_data_auto ("provider_salt",
&AH_provider_salt));
}
diff --git a/src/backend/anastasis-httpd_policy-upload.c b/src/backend/anastasis-httpd_policy-upload.c
@@ -366,11 +366,14 @@ proposal_cb (struct PolicyUploadContext *puc,
"Storing payment request for order `%s'\n",
por->details.ok.order_id);
+ /* The row records the asking price in the primary currency: which of
+ the offered currencies the wallet will settle in is not known until
+ it actually pays. */
qs = ANASTASIS_DB_do_insert_recdoc_payment (
&puc->account,
(uint32_t) AH_post_counter,
&puc->payment_identifier,
- &AH_annual_fee);
+ AH_primary_price (&AH_annual_fees));
if (0 >= qs)
{
GNUNET_break (0);
@@ -441,19 +444,12 @@ check_payment_cb (struct PolicyUploadContext *puc,
enum GNUNET_DB_QueryStatus qs;
unsigned int years;
struct GNUNET_TIME_Relative paid_until;
- const json_t *contract;
struct TALER_Amount amount;
- struct GNUNET_JSON_Specification cspec[] = {
- TALER_JSON_spec_amount_any ("amount",
- &amount),
- GNUNET_JSON_spec_end ()
- };
+ const struct TALER_Amount *fee;
- contract = osr->details.ok.details.paid.contract_terms;
if (GNUNET_OK !=
- GNUNET_JSON_parse (contract,
- cspec,
- NULL, NULL))
+ AH_paid_amount (osr,
+ &amount))
{
GNUNET_break (0);
puc->resp = TALER_MHD_make_error (
@@ -462,8 +458,24 @@ check_payment_cb (struct PolicyUploadContext *puc,
puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
return; /* continue as planned */
}
+ /* Divide by the fee in the currency actually paid, not by "the"
+ fee: with several on offer the two need not be the same one.
+ Reachable in production if an operator drops a currency while an
+ order priced in it is still unpaid, hence an error and not an
+ assertion. */
+ fee = TALER_amount_list_find (&AH_annual_fees,
+ amount.currency);
+ if (NULL == fee)
+ {
+ GNUNET_break (0);
+ puc->resp = TALER_MHD_make_error (
+ TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR,
+ "order was paid in a currency this provider does not offer");
+ puc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return; /* continue as planned */
+ }
years = TALER_amount_divide2 (&amount,
- &AH_annual_fee);
+ fee);
paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
years);
/* add 1 week grace period, otherwise if a user
@@ -580,14 +592,16 @@ begin_payment (struct PolicyUploadContext *puc)
AH_backend_url);
{
char *order_id;
- struct TALER_Amount upload_fee;
+ struct TALER_AmountList upload_fees;
- if (0 >
- TALER_amount_multiply (&upload_fee,
- &AH_annual_fee,
- puc->years_to_pay))
+ TALER_amount_list_copy (&upload_fees,
+ &AH_annual_fees);
+ if (GNUNET_OK !=
+ TALER_amount_list_multiply (&upload_fees,
+ puc->years_to_pay))
{
GNUNET_break_op (0);
+ TALER_amount_list_free (&upload_fees);
return TALER_MHD_reply_with_error (puc->con,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
@@ -600,24 +614,20 @@ begin_payment (struct PolicyUploadContext *puc)
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Creating order for %u years with payment of %s\n",
puc->years_to_pay,
- TALER_amount2s (&upload_fee));
- order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s }",
- "amount", TALER_JSON_from_amount (&upload_fee),
- "summary", "Anastasis policy storage fee",
- "products",
- "description", "policy storage fee",
- "quantity", (json_int_t) puc->years_to_pay,
- "unit", "years",
- "order_id", order_id);
+ TALER_amount_list2s (&upload_fees));
+ order = AH_make_order (order_id,
+ "Anastasis policy storage fee",
+ &upload_fees);
+ TALER_amount_list_free (&upload_fees);
GNUNET_free (order_id);
- if (NULL == order)
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (puc->con,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE,
- "could not create order");
- }
+ GNUNET_assert (0 ==
+ json_object_set_new (
+ order,
+ "products",
+ json_pack ("[{s:s,s:I,s:s}]",
+ "description", "policy storage fee",
+ "quantity", (json_int_t) puc->years_to_pay,
+ "unit", "years")));
}
/* Only link into the list once we are actually about to suspend: the list
exists to resume suspended connections at shutdown, and cleanup_ctx()
@@ -951,7 +961,12 @@ AH_handler_policy_post (
/* generate fresh payment identifier */
GNUNET_CRYPTO_random_block (&puc->payment_identifier,
sizeof (struct ANASTASIS_PaymentSecretP));
- if (! TALER_amount_is_zero (&AH_annual_fee))
+ /* GNUNET_SYSERR is unreachable: a price list that mixes free and
+ non-free currencies makes anastasis-httpd refuse to start. */
+ GNUNET_assert (GNUNET_SYSERR !=
+ TALER_amount_list_check_uniform (&AH_annual_fees));
+ if (GNUNET_NO !=
+ TALER_amount_list_check_uniform (&AH_annual_fees))
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"No payment identifier, requesting payment\n");
@@ -964,7 +979,7 @@ AH_handler_policy_post (
account_pub,
AH_post_counter,
&puc->payment_identifier,
- &AH_annual_fee);
+ AH_primary_price (&AH_annual_fees));
if (qs <= 0)
return TALER_MHD_reply_with_error (puc->con,
MHD_HTTP_INTERNAL_SERVER_ERROR,
diff --git a/src/backend/anastasis-httpd_truth-challenge.c b/src/backend/anastasis-httpd_truth-challenge.c
@@ -86,9 +86,17 @@ struct ChallengeContext
struct ANASTASIS_CRYPTO_TruthKeyP truth_key;
/**
- * Cost for paying the challenge.
+ * Cost for paying the challenge, one entry per currency the wallet may
+ * settle in. Borrowed from the authorization plugin or from
+ * #AH_question_costs, both of which outlive the request.
*/
- struct TALER_Amount challenge_cost;
+ const struct TALER_AmountList *challenge_costs;
+
+ /**
+ * What the wallet actually paid, in the currency it chose. Only
+ * meaningful if @e have_payment is true.
+ */
+ struct TALER_Amount challenge_paid;
/**
* Our handler context.
@@ -171,6 +179,12 @@ struct ChallengeContext
bool no_payment_identifier_provided;
/**
+ * True if @e challenge_paid holds what the wallet paid for this
+ * challenge, as read back from the database.
+ */
+ bool have_payment;
+
+ /**
* True if this entry is in the #gc_head DLL.
*/
bool in_list;
@@ -443,12 +457,18 @@ begin_refund (const struct ChallengeContext *gc)
re->order_id);
re->payment_identifier = gc->payment_identifier;
re->truth_uuid = gc->truth_uuid;
- re->ro = TALER_MERCHANT_post_private_orders_refund_create (AH_ctx,
- AH_backend_url,
- re->order_id,
- &gc->challenge_cost
- ,
- "failed to issue challenge");
+ /* Refund what was actually paid, in the currency it was paid in ---
+ with several currencies on offer the configured price no longer
+ identifies it. The fallback is for a payment_plugin_managed method,
+ where the order was never ours to begin with. */
+ re->ro = TALER_MERCHANT_post_private_orders_refund_create (
+ AH_ctx,
+ AH_backend_url,
+ re->order_id,
+ gc->have_payment
+ ? &gc->challenge_paid
+ : AH_primary_price (gc->challenge_costs),
+ "failed to issue challenge");
{
enum TALER_ErrorCode ec;
@@ -640,10 +660,13 @@ proposal_cb (struct ChallengeContext *gc,
gc->response_code = MHD_HTTP_BAD_GATEWAY;
return;
}
+ /* Records the asking price in the primary currency; which currency the
+ wallet settles in is only known once it has paid, and is written by
+ ANASTASIS_DB_update_to_challenge_payment_paid() then. */
qs = ANASTASIS_DB_insert_challenge_payment (
&gc->truth_uuid,
&gc->payment_identifier,
- &gc->challenge_cost);
+ AH_primary_price (gc->challenge_costs));
if (0 >= qs)
{
GNUNET_break (0);
@@ -730,9 +753,22 @@ check_payment_cb (struct ChallengeContext *gc,
{
enum GNUNET_DB_QueryStatus qs;
+ if (GNUNET_OK !=
+ AH_paid_amount (osr,
+ &gc->challenge_paid))
+ {
+ GNUNET_break (0);
+ gc->resp = TALER_MHD_make_error (
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ "no amount given");
+ gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return;
+ }
+ gc->have_payment = true;
qs = ANASTASIS_DB_update_to_challenge_payment_paid (
&gc->truth_uuid,
- &gc->payment_identifier);
+ &gc->payment_identifier,
+ &gc->challenge_paid);
if (0 <= qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
@@ -836,17 +872,17 @@ begin_payment (struct ChallengeContext *gc)
{
json_t *order;
- order = GNUNET_JSON_PACK (
- TALER_JSON_pack_amount ("amount",
- &gc->challenge_cost),
- GNUNET_JSON_pack_string ("summary",
- "challenge fee for anastasis service"),
- GNUNET_JSON_pack_string ("order_id",
- order_id),
- GNUNET_JSON_pack_time_rel ("auto_refund",
- AUTO_REFUND_TIMEOUT),
- GNUNET_JSON_pack_timestamp ("pay_deadline",
- pay_deadline));
+ order = AH_make_order (order_id,
+ "challenge fee for anastasis service",
+ gc->challenge_costs);
+ GNUNET_assert (0 ==
+ json_object_update_new (
+ order,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_time_rel ("auto_refund",
+ AUTO_REFUND_TIMEOUT),
+ GNUNET_JSON_pack_timestamp ("pay_deadline",
+ pay_deadline))));
gc->po = TALER_MERCHANT_post_private_orders_create (AH_ctx,
AH_backend_url,
order);
@@ -1167,13 +1203,18 @@ AH_handler_truth_challenge (
return ret;
}
- gc->challenge_cost = gc->authorization->cost;
+ gc->challenge_costs = &gc->authorization->costs;
GNUNET_free (method);
}
if (! gc->authorization->payment_plugin_managed)
{
- if (! TALER_amount_is_zero (&gc->challenge_cost))
+ /* GNUNET_SYSERR is unreachable: anastasis-httpd refuses to start on
+ a price list that mixes free and non-free currencies. */
+ GNUNET_assert (GNUNET_SYSERR !=
+ TALER_amount_list_check_uniform (gc->challenge_costs));
+ if (GNUNET_NO !=
+ TALER_amount_list_check_uniform (gc->challenge_costs))
{
/* Check database to see if the transaction is paid for */
enum GNUNET_DB_QueryStatus qs;
@@ -1190,7 +1231,8 @@ AH_handler_truth_challenge (
qs = ANASTASIS_DB_get_challenge_payment (
&gc->payment_identifier,
&gc->truth_uuid,
- &paid);
+ &paid,
+ &gc->challenge_paid);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
@@ -1220,6 +1262,7 @@ AH_handler_truth_challenge (
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Payment confirmed\n");
+ gc->have_payment = true;
break;
}
}
diff --git a/src/backend/anastasis-httpd_truth-solve.c b/src/backend/anastasis-httpd_truth-solve.c
@@ -75,9 +75,17 @@ struct SolveContext
struct ANASTASIS_CRYPTO_TruthKeyP truth_key;
/**
- * Cost for paying the challenge.
+ * Cost for paying the challenge, one entry per currency the wallet may
+ * settle in. Borrowed from the authorization plugin or from
+ * #AH_question_costs, both of which outlive the request.
*/
- struct TALER_Amount challenge_cost;
+ const struct TALER_AmountList *challenge_costs;
+
+ /**
+ * What the wallet actually paid, in the currency it chose. Only
+ * meaningful if @e have_payment is true.
+ */
+ struct TALER_Amount challenge_paid;
/**
* Our handler context.
@@ -166,6 +174,12 @@ struct SolveContext
bool no_payment_identifier_provided;
/**
+ * True if @e challenge_paid holds what the wallet paid for this
+ * challenge, as read back from the database.
+ */
+ bool have_payment;
+
+ /**
* True if this entry is in the #gc_head DLL.
*/
bool in_list;
@@ -475,10 +489,13 @@ proposal_cb (struct SolveContext *gc,
gc->response_code = MHD_HTTP_BAD_GATEWAY;
return;
}
+ /* Records the asking price in the primary currency; which currency the
+ wallet settles in is only known once it has paid, and is written by
+ ANASTASIS_DB_update_to_challenge_payment_paid() then. */
qs = ANASTASIS_DB_insert_challenge_payment (
&gc->truth_uuid,
&gc->payment_identifier,
- &gc->challenge_cost);
+ AH_primary_price (gc->challenge_costs));
if (0 >= qs)
{
GNUNET_break (0);
@@ -565,9 +582,22 @@ check_payment_cb (struct SolveContext *gc,
{
enum GNUNET_DB_QueryStatus qs;
+ if (GNUNET_OK !=
+ AH_paid_amount (osr,
+ &gc->challenge_paid))
+ {
+ GNUNET_break (0);
+ gc->resp = TALER_MHD_make_error (
+ TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID,
+ "no amount given");
+ gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ return;
+ }
+ gc->have_payment = true;
qs = ANASTASIS_DB_update_to_challenge_payment_paid (
&gc->truth_uuid,
- &gc->payment_identifier);
+ &gc->payment_identifier,
+ &gc->challenge_paid);
if (0 <= qs)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
@@ -671,17 +701,17 @@ begin_payment (struct SolveContext *gc)
{
json_t *order;
- order = GNUNET_JSON_PACK (
- TALER_JSON_pack_amount ("amount",
- &gc->challenge_cost),
- GNUNET_JSON_pack_string ("summary",
- "challenge fee for anastasis service"),
- GNUNET_JSON_pack_string ("order_id",
- order_id),
- GNUNET_JSON_pack_time_rel ("auto_refund",
- AUTO_REFUND_TIMEOUT),
- GNUNET_JSON_pack_timestamp ("pay_deadline",
- pay_deadline));
+ order = AH_make_order (order_id,
+ "challenge fee for anastasis service",
+ gc->challenge_costs);
+ GNUNET_assert (0 ==
+ json_object_update_new (
+ order,
+ GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_time_rel ("auto_refund",
+ AUTO_REFUND_TIMEOUT),
+ GNUNET_JSON_pack_timestamp ("pay_deadline",
+ pay_deadline))));
gc->po = TALER_MERCHANT_post_private_orders_create (AH_ctx,
AH_backend_url,
order);
@@ -1307,11 +1337,11 @@ AH_handler_truth_solve (
GNUNET_free (method);
return ret;
}
- gc->challenge_cost = gc->authorization->cost;
+ gc->challenge_costs = &gc->authorization->costs;
}
else
{
- gc->challenge_cost = AH_question_cost;
+ gc->challenge_costs = &AH_question_costs;
}
GNUNET_free (method);
}
@@ -1320,7 +1350,12 @@ AH_handler_truth_solve (
if ( (is_question) ||
(! gc->authorization->payment_plugin_managed) )
{
- if (! TALER_amount_is_zero (&gc->challenge_cost))
+ /* GNUNET_SYSERR is unreachable: anastasis-httpd refuses to start on
+ a price list that mixes free and non-free currencies. */
+ GNUNET_assert (GNUNET_SYSERR !=
+ TALER_amount_list_check_uniform (gc->challenge_costs));
+ if (GNUNET_NO !=
+ TALER_amount_list_check_uniform (gc->challenge_costs))
{
/* Check database to see if the transaction is paid for */
enum GNUNET_DB_QueryStatus qs;
@@ -1337,7 +1372,8 @@ AH_handler_truth_solve (
qs = ANASTASIS_DB_get_challenge_payment (
&gc->payment_identifier,
&gc->truth_uuid,
- &paid);
+ &paid,
+ &gc->challenge_paid);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
@@ -1367,6 +1403,7 @@ AH_handler_truth_solve (
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Payment confirmed\n");
+ gc->have_payment = true;
break;
}
}
diff --git a/src/backend/anastasis-httpd_truth-upload.c b/src/backend/anastasis-httpd_truth-upload.c
@@ -96,9 +96,10 @@ struct TruthUploadContext
struct GNUNET_TIME_Absolute timeout;
/**
- * Fee that is to be paid for this upload.
+ * Fee that is to be paid for this upload, one entry per currency the
+ * wallet may choose to settle in.
*/
- struct TALER_Amount upload_fee;
+ struct TALER_AmountList upload_fees;
/**
* HTTP response code to use on resume, if resp is set.
@@ -168,6 +169,7 @@ cleanup_truth_post (struct TM_HandlerContext *hc)
MHD_destroy_response (tuc->resp);
if (NULL != tuc->json)
json_decref (tuc->json);
+ TALER_amount_list_free (&tuc->upload_fees);
GNUNET_free (tuc);
}
@@ -323,19 +325,12 @@ check_payment_cb (struct TruthUploadContext *tuc,
enum GNUNET_DB_QueryStatus qs;
unsigned int years;
struct GNUNET_TIME_Relative paid_until;
- const json_t *contract;
struct TALER_Amount amount;
- struct GNUNET_JSON_Specification cspec[] = {
- TALER_JSON_spec_amount_any ("amount",
- &amount),
- GNUNET_JSON_spec_end ()
- };
+ const struct TALER_Amount *fee;
- contract = osr->details.ok.details.paid.contract_terms;
if (GNUNET_OK !=
- GNUNET_JSON_parse (contract,
- cspec,
- NULL, NULL))
+ AH_paid_amount (osr,
+ &amount))
{
GNUNET_break (0);
tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
@@ -344,8 +339,21 @@ check_payment_cb (struct TruthUploadContext *tuc,
"contract terms in database are malformed");
break;
}
+ /* Divide by the fee in the currency actually paid: with several
+ on offer, "the" fee is not well defined. */
+ fee = TALER_amount_list_find (&AH_truth_upload_fees,
+ amount.currency);
+ if (NULL == fee)
+ {
+ GNUNET_break (0);
+ tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ tuc->resp = TALER_MHD_make_error (
+ TALER_EC_ANASTASIS_GENERIC_BACKEND_ERROR,
+ "order was paid in a currency this provider does not offer");
+ break;
+ }
years = TALER_amount_divide2 (&amount,
- &AH_truth_upload_fee);
+ fee);
paid_until = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_YEARS,
years);
/* add 1 week grace period, otherwise if a user
@@ -411,27 +419,19 @@ check_payment_cb (struct TruthUploadContext *tuc,
"%u, setting up fresh order %s\n",
MHD_HTTP_NOT_FOUND,
order_id);
- order = json_pack ("{s:o, s:s, s:[{s:s,s:I,s:s}], s:s}",
- "amount",
- TALER_JSON_from_amount (&tuc->upload_fee),
- "summary",
- "Anastasis challenge storage fee",
- "products",
- "description", "challenge storage fee",
- "quantity", (json_int_t) tuc->years_to_pay,
- "unit", "years",
- "order_id",
- order_id);
+ order = AH_make_order (order_id,
+ "Anastasis challenge storage fee",
+ &tuc->upload_fees);
GNUNET_free (order_id);
- if (NULL == order)
- {
- GNUNET_break (0);
- tuc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
- tuc->resp = TALER_MHD_make_error (
- TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE,
- "could not create order");
- break;
- }
+ GNUNET_assert (0 ==
+ json_object_set_new (
+ order,
+ "products",
+ json_pack ("[{s:s,s:I,s:s}]",
+ "description", "challenge storage fee",
+ "quantity",
+ (json_int_t) tuc->years_to_pay,
+ "unit", "years")));
tuc->po = TALER_MERCHANT_post_private_orders_create (AH_ctx,
AH_backend_url,
order);
@@ -652,7 +652,12 @@ AH_handler_truth_post (
if (0 == storage_years)
storage_years = 1;
- if (! TALER_amount_is_zero (&AH_truth_upload_fee))
+ /* GNUNET_SYSERR is unreachable: anastasis-httpd refuses to start on a
+ price list that mixes free and non-free currencies. */
+ GNUNET_assert (GNUNET_SYSERR !=
+ TALER_amount_list_check_uniform (&AH_truth_upload_fees));
+ if (GNUNET_NO !=
+ TALER_amount_list_check_uniform (&AH_truth_upload_fees))
{
struct GNUNET_TIME_Timestamp desired_until;
enum GNUNET_DB_QueryStatus qs;
@@ -687,10 +692,12 @@ AH_handler_truth_post (
/ GNUNET_TIME_UNIT_YEARS.rel_value_us;
if (0 != (rem.rel_value_us % GNUNET_TIME_UNIT_YEARS.rel_value_us))
tuc->years_to_pay++;
- if (0 >
- TALER_amount_multiply (&tuc->upload_fee,
- &AH_truth_upload_fee,
- tuc->years_to_pay))
+ TALER_amount_list_free (&tuc->upload_fees);
+ TALER_amount_list_copy (&tuc->upload_fees,
+ &AH_truth_upload_fees);
+ if (GNUNET_OK !=
+ TALER_amount_list_multiply (&tuc->upload_fees,
+ tuc->years_to_pay))
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
@@ -699,7 +706,7 @@ AH_handler_truth_post (
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"storage_duration_years");
}
- if (! TALER_amount_is_zero (&tuc->upload_fee))
+ if (0 != tuc->upload_fees.tal_len)
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Truth upload payment required (%d)!\n",
diff --git a/src/backend/anastasis.conf b/src/backend/anastasis.conf
@@ -38,17 +38,46 @@ DB = postgres
# Display name of the business running this anastasis provider.
# BUSINESS_NAME = ...
-# Annual fee for an account
-# ANNUAL_FEE = TESTKUDOS:0.1
+# Currencies this provider prices its service in, separated by ";". The
+# first one is the primary currency: it is what the scalar fields of
+# /config report, for the benefit of clients that predate multi-currency
+# support. Every priced option below must give a price in exactly these
+# currencies --- anastasis-httpd refuses to start otherwise, so that a
+# user cannot discover during recovery that some step is unavailable in
+# the currency they hold.
+#
+# May be omitted by a provider that prices in a single currency; it is
+# then taken from the fees themselves.
+# CURRENCIES = EUR;CHF;USD
+
+# Annual fee for an account, one amount per currency in CURRENCIES.
+# ANNUAL_FEE = EUR:0.1;CHF:0.1;USD:0.12
# Number of policy uploads included in one annual fee payment
ANNUAL_POLICY_UPLOAD_LIMIT = 64
# Insurance
-# INSURANCE = TESTKUDOS:1.0
+# INSURANCE = EUR:1.0;CHF:1.0;USD:1.2
# Fee for one upload.
-# TRUTH_UPLOAD_FEE = TESTKUDOS:0.1
+# TRUTH_UPLOAD_FEE = EUR:0.1;CHF:0.1;USD:0.12
+
+# Each authorization method is priced the same way, in its own
+# [authorization-*] section. A method may also be free, which is what an
+# IBAN challenge usually wants: the wire transfer the user has to make in
+# order to authenticate is already the cost of using it. Free is written
+# as an empty value, or as a zero in every currency:
+#
+# [authorization-iban]
+# ENABLED = YES
+# COST =
+#
+# What is *not* allowed is charging in some currencies and not in others,
+# e.g. "COST = EUR:1;CHF:0": that does not price the service, it prices
+# the user's choice of currency, and anastasis-httpd refuses to start on
+# it. Note that the amount the user must transfer for an IBAN challenge
+# is a separate option and stays a single amount, since a bank account
+# has one currency.
# Upload limit per backup, in megabytes
diff --git a/src/cli/meson.build b/src/cli/meson.build
@@ -1,5 +1,6 @@
# This file is in the public domain
check_SCRIPTS = [
+ 'test_anastasis_config_currencies',
'test_anastasis_reducer_initialize_state',
'test_anastasis_reducer_select_continent',
'test_anastasis_reducer_select_country',
diff --git a/src/cli/setup.sh b/src/cli/setup.sh
@@ -43,9 +43,28 @@ function setup()
SETUP_PID=$!
# Close FD3
exec 3>&-
- sed -u '/READY:/ q' <&4
+ # Pass the setup's output through until it announces READY:. Note that
+ # reaching end-of-pipe is NOT success: that is what happens when
+ # taler-unified-setup.sh dies during startup, and continuing then makes
+ # the test fail much later against a system that was never brought up,
+ # hiding the actual error. So insist on having seen the marker.
+ SETUP_READY=0
+ while IFS= read -r line <&4
+ do
+ printf '%s\n' "$line"
+ case "$line" in
+ *READY:*)
+ SETUP_READY=1
+ break
+ ;;
+ esac
+ done
# Close FD4
exec 4>&-
+ if [ "1" != "$SETUP_READY" ]
+ then
+ exit_fail "taler-unified-setup.sh failed to start the test system"
+ fi
echo "Test system ready" >&2
}
diff --git a/src/cli/test_anastasis_config_currencies.sh b/src/cli/test_anastasis_config_currencies.sh
@@ -0,0 +1,177 @@
+#!/bin/bash
+# This file is part of Anastasis
+# Copyright (C) 2026 Anastasis SARL
+#
+# Anastasis 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.
+#
+# Anastasis 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License along with
+# Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+#
+# Check the multi-currency configuration rules that anastasis-httpd enforces
+# at startup: a price must be uniformly free or uniformly priced, and a
+# priced option must cover exactly [anastasis] CURRENCIES.
+#
+# Needs neither a database nor any Taler service. Every case below is
+# decided while the configuration is parsed, which happens before
+# anastasis-httpd connects to anything.
+set -eu
+
+# Exit code for skipped tests, as expected by meson.
+SKIP=77
+
+if ! anastasis-httpd --help > /dev/null 2> /dev/null
+then
+ echo "anastasis-httpd not found, skipping"
+ exit $SKIP
+fi
+
+WORK_DIR=$(mktemp -p "${TMPDIR:-/tmp}" -d test-anastasis-currencies-XXXXXX)
+# shellcheck disable=SC2317
+cleanup () {
+ rm -rf "$WORK_DIR"
+}
+trap cleanup EXIT
+
+BASE="$(pwd)/test_reducer.conf"
+LOG="$WORK_DIR/httpd.log"
+
+# Start anastasis-httpd on test_reducer.conf plus the given overrides and
+# report whether the price lists were accepted. Prints "accepted" or
+# "refused"; the log is left in $LOG.
+#
+# The verdict is read off the log rather than off the exit status on
+# purpose: this configuration is deliberately incomplete in other ways
+# (no BUSINESS_NAME, no initialised database), so the process stops
+# either way and only *why* it stopped distinguishes the cases.
+try_config ()
+{
+ cat > "$WORK_DIR/test.conf" <<EOF
+@INLINE@ $BASE
+$1
+EOF
+ anastasis-httpd -c "$WORK_DIR/test.conf" > "$LOG" 2>&1 &
+ local pid=$!
+ sleep 1
+ kill "$pid" 2> /dev/null || true
+ wait "$pid" 2> /dev/null || true
+ if grep -q "Price list error" "$LOG"
+ then
+ echo "refused"
+ else
+ echo "accepted"
+ fi
+}
+
+fail ()
+{
+ cat "$LOG"
+ echo "FAIL: $1"
+ exit 1
+}
+
+# A configuration that prices everything in both currencies; each case
+# below deviates from it in exactly one option.
+GOOD="[anastasis]
+CURRENCIES = TESTKUDOS;EUR
+ANNUAL_FEE = TESTKUDOS:4.99;EUR:4
+TRUTH_UPLOAD_FEE = TESTKUDOS:0.01;EUR:0.01
+INSURANCE = TESTKUDOS:1.0;EUR:1.0
+
+[authorization-question]
+COST = TESTKUDOS:0.0;EUR:0.0"
+
+echo -n "Test that a well-formed multi-currency configuration is accepted ..."
+RES=$(try_config "$GOOD")
+if [ "$RES" != "accepted" ]
+then
+ fail "anastasis-httpd rejected a well-formed multi-currency configuration"
+fi
+echo " OK"
+
+echo -n "Test that a mixed free/priced option is refused ..."
+RES=$(try_config "${GOOD}
+
+[anastasis]
+ANNUAL_FEE = TESTKUDOS:4.99;EUR:0")
+if [ "$RES" != "refused" ]
+then
+ fail "anastasis-httpd accepted ANNUAL_FEE = TESTKUDOS:4.99;EUR:0"
+fi
+# The diagnostic has to name the option, or an operator cannot act on it.
+if ! grep -q "ANNUAL_FEE" "$LOG"
+then
+ fail "refusal did not name the offending option"
+fi
+echo " OK"
+
+echo -n "Test that an option missing a currency is refused ..."
+RES=$(try_config "${GOOD}
+
+[anastasis]
+TRUTH_UPLOAD_FEE = TESTKUDOS:0.01")
+if [ "$RES" != "refused" ]
+then
+ fail "anastasis-httpd accepted a TRUTH_UPLOAD_FEE missing EUR"
+fi
+if ! grep -q "TRUTH_UPLOAD_FEE" "$LOG" || ! grep -q "EUR" "$LOG"
+then
+ fail "refusal did not name the option and the missing currency"
+fi
+echo " OK"
+
+echo -n "Test that an option priced in an undeclared currency is refused ..."
+RES=$(try_config "${GOOD}
+
+[anastasis]
+TRUTH_UPLOAD_FEE = TESTKUDOS:0.01;EUR:0.01;CHF:0.01")
+if [ "$RES" != "refused" ]
+then
+ fail "anastasis-httpd accepted a fee in a currency not in CURRENCIES"
+fi
+if ! grep -q "CHF" "$LOG"
+then
+ fail "refusal did not name the undeclared currency"
+fi
+echo " OK"
+
+echo -n "Test that an option free in every currency is accepted ..."
+# Zero is a first-class answer: it is how an operator says a method costs
+# nothing, e.g. because the IBAN transfer is already the cost of using it.
+RES=$(try_config "${GOOD}
+
+[authorization-question]
+COST = TESTKUDOS:0;EUR:0")
+if [ "$RES" != "accepted" ]
+then
+ fail "anastasis-httpd rejected an all-zero COST"
+fi
+echo " OK"
+
+echo -n "Test that an empty option means free ..."
+RES=$(try_config "${GOOD}
+
+[authorization-question]
+COST =")
+if [ "$RES" != "accepted" ]
+then
+ fail "anastasis-httpd rejected an empty COST"
+fi
+echo " OK"
+
+echo -n "Test that a single-currency configuration still works ..."
+# No CURRENCIES option at all: this is every configuration written before
+# multi-currency support, and none of them may break.
+RES=$(try_config "")
+if [ "$RES" != "accepted" ]
+then
+ fail "anastasis-httpd rejected a single-currency configuration"
+fi
+echo " OK"
+
+exit 0
diff --git a/src/cli/test_free_reducer.conf b/src/cli/test_free_reducer.conf
@@ -3,6 +3,9 @@
[anastasis]
CURRENCY = EUR
+# Currencies the service is priced in; every priced option
+# below must give a price in exactly these.
+CURRENCIES = EUR
DB = postgres
ANNUAL_FEE = EUR:0
TRUTH_UPLOAD_FEE = EUR:0
diff --git a/src/cli/test_reducer.conf b/src/cli/test_reducer.conf
@@ -8,6 +8,7 @@ TALER_RUNTIME_DIR = ${TMPDIR:-${TMP:-/tmp}}/${USER:-}/taler-system-runtime/
[anastasis]
CURRENCY = TESTKUDOS
+CURRENCIES = TESTKUDOS
DB = postgres
ANNUAL_FEE = TESTKUDOS:4.99
TRUTH_UPLOAD_FEE = TESTKUDOS:0.01
diff --git a/src/include/anastasis/anastasis-database/get_challenge_payment.h b/src/include/anastasis/anastasis-database/get_challenge_payment.h
@@ -33,12 +33,16 @@
* @param payment_secret payment secret which the user must provide with every upload
* @param truth_uuid which truth should we check the payment status of
* @param[out] paid bool value to show if payment is paid
+ * @param[out] amount set to the amount that was paid, in the currency it was
+ * paid in; only meaningful if @a paid is true, as the row still
+ * carries the asking price until the payment is confirmed
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_get_challenge_payment (
const struct ANASTASIS_PaymentSecretP *payment_secret,
const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
- bool *paid);
+ bool *paid,
+ struct TALER_Amount *amount);
#endif
diff --git a/src/include/anastasis/anastasis-database/update_auth_iban_in_currency.h b/src/include/anastasis/anastasis-database/update_auth_iban_in_currency.h
@@ -0,0 +1,48 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2026 Anastasis SARL
+
+ Anastasis 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.
+
+ Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file include/anastasis/anastasis-database/update_auth_iban_in_currency.h
+ * @brief Anastasis database: stamp the configured currency onto IBAN
+ * transfers migrated from before amounts carried one
+ * @author Christian Grothoff
+ */
+#ifndef ANASTASIS_DATABASE_UPDATE_AUTH_IBAN_IN_CURRENCY_H
+#define ANASTASIS_DATABASE_UPDATE_AUTH_IBAN_IN_CURRENCY_H
+
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_db_lib.h>
+#include "anastasis_service.h"
+#include "anastasis/anastasis-database/common.h"
+
+/**
+ * Fill in the currency of IBAN wire transfers recorded before amounts
+ * carried one.
+ *
+ * The stasis-0004 migration widened the amount columns to include a
+ * currency, but SQL cannot know which one: it lives in [anastasis]
+ * CURRENCY. The rows are therefore left with a NULL currency for this
+ * function to complete. Unlike the three payment tables, these rows
+ * cannot simply be stamped with a placeholder --- the IBAN plugin
+ * compares them against the amount it expects, so a wrong currency would
+ * silently invalidate every challenge still in flight across the upgrade.
+ *
+ * @return transaction status; #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if
+ * there was nothing left to fill in, which is the normal case
+ */
+enum GNUNET_DB_QueryStatus
+ANASTASIS_DB_update_auth_iban_in_currency (void);
+
+#endif
diff --git a/src/include/anastasis/anastasis-database/update_to_challenge_payment_paid.h b/src/include/anastasis/anastasis-database/update_to_challenge_payment_paid.h
@@ -31,11 +31,13 @@
*
* @param truth_uuid which challenge received a payment
* @param payment_identifier proof of payment, must be unique and match pending payment
+ * @param amount what was actually paid, in the currency it was paid in
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_update_to_challenge_payment_paid (
const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
- const struct ANASTASIS_PaymentSecretP *payment_identifier);
+ const struct ANASTASIS_PaymentSecretP *payment_identifier,
+ const struct TALER_Amount *amount);
#endif
diff --git a/src/include/anastasis_authorization_plugin.h b/src/include/anastasis_authorization_plugin.h
@@ -141,10 +141,13 @@ struct ANASTASIS_AuthorizationPlugin
void *cls;
/**
- * Cost to GET the /truth using this method. Set by the plugin's
- * loader, not by the plugin itself.
+ * Cost to GET the /truth using this method, one entry per currency
+ * the provider prices in. Empty (or zero throughout) means the method
+ * is free, which is what an IBAN-style method wants when the wire
+ * transfer the user has to make already is the cost of using it. Set
+ * by the plugin's loader, not by the plugin itself.
*/
- struct TALER_Amount cost;
+ struct TALER_AmountList costs;
/**
* True if the payment is managed internally by the
diff --git a/src/include/anastasis_database_lib.h b/src/include/anastasis_database_lib.h
@@ -49,6 +49,7 @@
#include "anastasis/anastasis-database/do_verify_challenge_code.h"
#include "anastasis/anastasis-database/update_to_challenge_code_satisfied.h"
#include "anastasis/anastasis-database/update_totp_counter.h"
+#include "anastasis/anastasis-database/update_auth_iban_in_currency.h"
#include "anastasis/anastasis-database/get_exists_challenge_code_satisfied.h"
#include "anastasis/anastasis-database/insert_challenge_code.h"
#include "anastasis/anastasis-database/update_to_challenge_code_sent.h"
diff --git a/src/include/anastasis_service.h b/src/include/anastasis_service.h
@@ -40,9 +40,19 @@ struct ANASTASIS_AuthorizationMethodConfig
const char *type;
/**
- * Fee charged for accessing key share using this method.
+ * Fee charged for accessing key share using this method, in the
+ * provider's primary currency. Kept because a provider speaking
+ * protocol version 2 offers nothing else.
*/
struct TALER_Amount usage_fee;
+
+ /**
+ * Fee charged for accessing key share using this method, one entry
+ * per currency the provider accepts. Synthesized from @e usage_fee
+ * for a provider that does not offer the plural field yet. Empty
+ * means the method is free.
+ */
+ struct TALER_AmountList usage_fees;
};
@@ -105,22 +115,53 @@ struct ANASTASIS_Config
uint32_t storage_limit_in_megabytes;
/**
- * Annual fee for an account / policy upload.
+ * Currencies the provider prices its service in, primary
+ * currency first. For a provider speaking protocol version 2
+ * this is the single currency of @e annual_fee.
+ */
+ const char **currencies;
+
+ /**
+ * Length of the @e currencies array.
+ */
+ unsigned int currencies_len;
+
+ /**
+ * Annual fee for an account / policy upload, in the primary
+ * currency.
*/
struct TALER_Amount annual_fee;
/**
- * Fee for a truth upload.
+ * Annual fee for an account / policy upload, per currency.
+ * Synthesized from @e annual_fee for a provider speaking
+ * protocol version 2.
+ */
+ struct TALER_AmountList annual_fees;
+
+ /**
+ * Fee for a truth upload, in the primary currency.
*/
struct TALER_Amount truth_upload_fee;
/**
+ * Fee for a truth upload, per currency.
+ */
+ struct TALER_AmountList truth_upload_fees;
+
+ /**
* Maximum legal liability for data loss covered by the
- * provider.
+ * provider, in the primary currency.
*/
struct TALER_Amount liability_limit;
/**
+ * Maximum legal liability for data loss covered by the
+ * provider, per currency.
+ */
+ struct TALER_AmountList liability_limits;
+
+ /**
* Provider salt.
*/
struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
diff --git a/src/reducer/anastasis_api_backup_redux.c b/src/reducer/anastasis_api_backup_redux.c
@@ -69,6 +69,67 @@ struct Costs
/**
+ * Pick the single amount a provider contributes to an advisory total.
+ *
+ * A price list is not a sum: a provider offering both EUR and CHF wants
+ * one or the other, not both, so adding every entry would tell the user
+ * they owe twice over. Each provider therefore contributes exactly one
+ * amount --- in the preferred currency if it offers one, and otherwise in
+ * its own primary currency, which is the honest answer that this part of
+ * the backup will be settled separately.
+ *
+ * @param common state holding the user's preferred currency
+ * @param al price list to pick from
+ * @return the amount to add, NULL if @a al is empty
+ */
+static const struct TALER_Amount *
+preferred_price (const struct ANASTASIS_ReduxCommon *common,
+ const struct TALER_AmountList *al)
+{
+ if (NULL != common->preferred_currency)
+ {
+ const struct TALER_Amount *a;
+
+ a = TALER_amount_list_find (al,
+ common->preferred_currency);
+ if (NULL != a)
+ return a;
+ }
+ if (0 == al->tal_len)
+ return NULL;
+ return &al->tal[0];
+}
+
+
+/**
+ * Check if two price lists offer the same prices, ignoring order.
+ *
+ * @param a first list
+ * @param b second list
+ * @return true if they agree on every currency
+ */
+static bool
+amount_lists_equal (const struct TALER_AmountList *a,
+ const struct TALER_AmountList *b)
+{
+ if (a->tal_len != b->tal_len)
+ return false;
+ for (unsigned int i = 0; i<a->tal_len; i++)
+ {
+ const struct TALER_Amount *o;
+
+ o = TALER_amount_list_find (b,
+ a->tal[i].currency);
+ if ( (NULL == o) ||
+ (0 != TALER_amount_cmp (&a->tal[i],
+ o)) )
+ return false;
+ }
+ return true;
+}
+
+
+/**
* Add amount from @a cost to @a my_cost list.
*
* @param[in,out] my_cost pointer to list to modify
@@ -773,12 +834,11 @@ equiv_provider (const struct PolicyBuilder *pb,
}
c1 = &j1->config;
c2 = &j2->config;
- if ( (GNUNET_OK !=
- TALER_amount_cmp_currency (&c1->truth_upload_fee,
- &c2->truth_upload_fee)) ||
- (0 !=
- TALER_amount_cmp (&c1->truth_upload_fee,
- &c2->truth_upload_fee)) )
+ /* Compare the whole price lists, not just the primary currency: two
+ providers that agree on EUR but differ on which other currencies
+ they take are not interchangeable for a user holding one of them. */
+ if (! amount_lists_equal (&c1->truth_upload_fees,
+ &c2->truth_upload_fees))
return false;
if (c1->methods_len != c2->methods_len)
@@ -794,11 +854,8 @@ equiv_provider (const struct PolicyBuilder *pb,
if ( (0 == strcmp (m1->type,
m2->type)) &&
- (GNUNET_OK ==
- TALER_amount_cmp_currency (&m1->usage_fee,
- &m2->usage_fee)) &&
- (0 == TALER_amount_cmp (&m1->usage_fee,
- &m2->usage_fee)) )
+ amount_lists_equal (&m1->usage_fees,
+ &m2->usage_fees) )
{
matched = true;
break;
@@ -858,12 +915,20 @@ eval_provider_selection (struct PolicyBuilder *pb,
(challenge_size_ok (cfg->storage_limit_in_megabytes,
am->challenge_size) ) )
{
- found = true;
+ const struct TALER_Amount *uf;
+ const struct TALER_Amount *tf;
- add_cost (&policy_ent[i].usage_fee,
- &ms->usage_fee);
- add_cost (&policy_ent[i].usage_fee,
- &cfg->truth_upload_fee);
+ found = true;
+ uf = preferred_price (pb->common,
+ &ms->usage_fees);
+ tf = preferred_price (pb->common,
+ &cfg->truth_upload_fees);
+ if (NULL != uf)
+ add_cost (&policy_ent[i].usage_fee,
+ uf);
+ if (NULL != tf)
+ add_cost (&policy_ent[i].usage_fee,
+ tf);
}
}
if (! found)
@@ -2167,19 +2232,78 @@ update_expiration_cost (struct ANASTASIS_ReduxState *rs,
years = expiration_to_years (expiration);
+ /* Publish the currencies in which the *whole* backup can be settled
+ with one choice of currency, i.e. the intersection over the
+ providers that will actually be used. An empty intersection is not
+ an error: it means the user will pay more than one order in more
+ than one currency, which is exactly what `upload_fees' then shows. */
+ {
+ bool first = true;
+
+ for (unsigned int i = 0; i < rs->common.currencies_len; i++)
+ GNUNET_free (rs->common.currencies[i]);
+ GNUNET_array_grow (rs->common.currencies,
+ rs->common.currencies_len,
+ 0);
+ rs->common.have_currencies = true;
+ for (unsigned int i = 0; i < rs->common.providers_len; i++)
+ {
+ const struct ANASTASIS_ReduxProvider *p = &rs->common.providers[i];
+
+ if ( (ANASTASIS_RPS_OK != p->status) ||
+ (! p->have_config) ||
+ (MHD_HTTP_OK != p->config.http_status) )
+ continue;
+ if (first)
+ {
+ for (unsigned int j = 0; j < p->config.currencies_len; j++)
+ GNUNET_array_append (rs->common.currencies,
+ rs->common.currencies_len,
+ GNUNET_strdup (p->config.currencies[j]));
+ first = false;
+ continue;
+ }
+ for (unsigned int j = 0; j < rs->common.currencies_len;)
+ {
+ bool found = false;
+
+ for (unsigned int k = 0; k < p->config.currencies_len; k++)
+ if (0 == strcasecmp (rs->common.currencies[j],
+ p->config.currencies[k]))
+ found = true;
+ if (found)
+ {
+ j++;
+ continue;
+ }
+ GNUNET_free (rs->common.currencies[j]);
+ rs->common.currencies[j]
+ = rs->common.currencies[rs->common.currencies_len - 1];
+ GNUNET_array_grow (rs->common.currencies,
+ rs->common.currencies_len,
+ rs->common.currencies_len - 1);
+ }
+ }
+ }
+
/* go over all providers and add up cost */
for (unsigned int i = 0; i < rs->common.providers_len; i++)
{
const struct ANASTASIS_ReduxProvider *p = &rs->common.providers[i];
+ const struct TALER_Amount *af;
struct TALER_Amount fee;
if ( (ANASTASIS_RPS_OK != p->status) ||
(! p->have_config) ||
(MHD_HTTP_OK != p->config.http_status) )
continue; /* skip providers that are down or disabled */
+ af = preferred_price (&rs->common,
+ &p->config.annual_fees);
+ if (NULL == af)
+ continue; /* provider is free */
if (0 >
TALER_amount_multiply (&fee,
- &p->config.annual_fee,
+ af,
years))
{
GNUNET_break (0);
@@ -2247,11 +2371,16 @@ update_expiration_cost (struct ANASTASIS_ReduxState *rs,
return GNUNET_SYSERR;
}
{
+ const struct TALER_Amount *tf;
struct TALER_Amount fee;
+ tf = preferred_price (&rs->common,
+ &p->config.truth_upload_fees);
+ if (NULL == tf)
+ continue; /* provider stores truths for free */
if (0 >
TALER_amount_multiply (&fee,
- &p->config.truth_upload_fee,
+ tf,
years))
{
GNUNET_break (0);
diff --git a/src/reducer/anastasis_api_redux.c b/src/reducer/anastasis_api_redux.c
@@ -108,9 +108,15 @@ struct AuthorizationMethodConfig
char *type;
/**
- * Fee charged for accessing key share using this method.
+ * Fee charged for accessing key share using this method, in the
+ * provider's primary currency.
*/
struct TALER_Amount usage_fee;
+
+ /**
+ * Fee charged for accessing key share using this method, per currency.
+ */
+ struct TALER_AmountList usage_fees;
};
@@ -191,22 +197,48 @@ struct ConfigRequest
uint32_t storage_limit_in_megabytes;
/**
- * Annual fee for an account / policy upload.
+ * Annual fee for an account / policy upload, primary currency.
*/
struct TALER_Amount annual_fee;
/**
- * Fee for a truth upload.
+ * Annual fee for an account / policy upload, per currency.
+ */
+ struct TALER_AmountList annual_fees;
+
+ /**
+ * Fee for a truth upload, primary currency.
*/
struct TALER_Amount truth_upload_fee;
/**
+ * Fee for a truth upload, per currency.
+ */
+ struct TALER_AmountList truth_upload_fees;
+
+ /**
* Maximum legal liability for data loss covered by the
- * provider.
+ * provider, primary currency.
*/
struct TALER_Amount liability_limit;
/**
+ * Maximum legal liability for data loss covered by the
+ * provider, per currency.
+ */
+ struct TALER_AmountList liability_limits;
+
+ /**
+ * Currencies the provider accepts, primary currency first.
+ */
+ char **currencies;
+
+ /**
+ * Length of the @e currencies array.
+ */
+ unsigned int currencies_len;
+
+ /**
* Provider salt.
*/
struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
@@ -289,7 +321,7 @@ ANASTASIS_REDUX_probe_external_reducer (void)
static enum ANASTASIS_GenericState
get_generic_state (const struct ANASTASIS_ReduxState *rs)
{
- unsigned int s;
+ unsigned int s = UINT_MAX;
switch (rs->type)
{
@@ -407,8 +439,19 @@ free_config_request (struct ConfigRequest *cr)
GNUNET_free (cr->url);
GNUNET_free (cr->business_name);
for (unsigned int i = 0; i<cr->methods_length; i++)
+ {
GNUNET_free (cr->methods[i].type);
+ TALER_amount_list_free (&cr->methods[i].usage_fees);
+ }
GNUNET_free (cr->methods);
+ TALER_amount_list_free (&cr->annual_fees);
+ TALER_amount_list_free (&cr->truth_upload_fees);
+ TALER_amount_list_free (&cr->liability_limits);
+ for (unsigned int i = 0; i<cr->currencies_len; i++)
+ GNUNET_free (cr->currencies[i]);
+ GNUNET_array_grow (cr->currencies,
+ cr->currencies_len,
+ 0);
GNUNET_free (cr);
}
@@ -549,10 +592,26 @@ notify_waiting (struct ConfigRequest *cr)
{
cfg->methods[i].type = GNUNET_strdup (cr->methods[i].type);
cfg->methods[i].usage_fee = cr->methods[i].usage_fee;
+ TALER_amount_list_copy (&cfg->methods[i].usage_fees,
+ &cr->methods[i].usage_fees);
}
cfg->annual_fee = cr->annual_fee;
+ TALER_amount_list_copy (&cfg->annual_fees,
+ &cr->annual_fees);
cfg->truth_upload_fee = cr->truth_upload_fee;
+ TALER_amount_list_copy (&cfg->truth_upload_fees,
+ &cr->truth_upload_fees);
cfg->liability_limit = cr->liability_limit;
+ TALER_amount_list_copy (&cfg->liability_limits,
+ &cr->liability_limits);
+ cfg->currencies_len = cr->currencies_len;
+ cfg->currencies = GNUNET_new_array (cfg->currencies_len,
+ char *);
+ for (unsigned int i = 0; i<cfg->currencies_len; i++)
+ cfg->currencies[i] = GNUNET_strdup (cr->currencies[i]);
+ if ( (NULL == cfg->currency) &&
+ (0 != cfg->currencies_len) )
+ cfg->currency = GNUNET_strdup (cfg->currencies[0]);
cfg->provider_salt = cr->provider_salt;
cfg->business_name = GNUNET_strdup (cr->business_name);
cfg->storage_limit_in_megabytes = cr->storage_limit_in_megabytes;
@@ -629,7 +688,10 @@ config_cb (void *cls,
GNUNET_free (cr->business_name);
cr->business_name = GNUNET_strdup (acfg->details.ok.business_name);
for (unsigned int i = 0; i<cr->methods_length; i++)
+ {
GNUNET_free (cr->methods[i].type);
+ TALER_amount_list_free (&cr->methods[i].usage_fees);
+ }
GNUNET_free (cr->methods);
cr->methods = GNUNET_new_array (acfg->details.ok.methods_length,
struct AuthorizationMethodConfig);
@@ -637,6 +699,8 @@ config_cb (void *cls,
{
cr->methods[i].type = GNUNET_strdup (acfg->details.ok.methods[i].type);
cr->methods[i].usage_fee = acfg->details.ok.methods[i].usage_fee;
+ TALER_amount_list_copy (&cr->methods[i].usage_fees,
+ &acfg->details.ok.methods[i].usage_fees);
}
cr->methods_length = acfg->details.ok.methods_length;
cr->storage_limit_in_megabytes =
@@ -644,6 +708,24 @@ config_cb (void *cls,
cr->annual_fee = acfg->details.ok.annual_fee;
cr->truth_upload_fee = acfg->details.ok.truth_upload_fee;
cr->liability_limit = acfg->details.ok.liability_limit;
+ TALER_amount_list_free (&cr->annual_fees);
+ TALER_amount_list_copy (&cr->annual_fees,
+ &acfg->details.ok.annual_fees);
+ TALER_amount_list_free (&cr->truth_upload_fees);
+ TALER_amount_list_copy (&cr->truth_upload_fees,
+ &acfg->details.ok.truth_upload_fees);
+ TALER_amount_list_free (&cr->liability_limits);
+ TALER_amount_list_copy (&cr->liability_limits,
+ &acfg->details.ok.liability_limits);
+ for (unsigned int i = 0; i<cr->currencies_len; i++)
+ GNUNET_free (cr->currencies[i]);
+ GNUNET_array_grow (cr->currencies,
+ cr->currencies_len,
+ 0);
+ for (unsigned int i = 0; i<acfg->details.ok.currencies_len; i++)
+ GNUNET_array_append (cr->currencies,
+ cr->currencies_len,
+ GNUNET_strdup (acfg->details.ok.currencies[i]));
cr->provider_salt = acfg->details.ok.provider_salt;
}
notify_waiting (cr);
@@ -1259,6 +1341,85 @@ select_country (struct ANASTASIS_ReduxState *rs,
ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING);
GNUNET_free (rs->common.selected_country);
rs->common.selected_country = GNUNET_strdup (country_code);
+ /* The country's currency is the default the user is offered; an
+ explicit "select_currency" overrides it. */
+ if (NULL == rs->common.preferred_currency)
+ for (unsigned int i = 0; i < rs->common.countries_len; i++)
+ if ( (0 == strcmp (rs->common.countries[i].code,
+ country_code)) &&
+ (NULL != rs->common.countries[i].currency) )
+ {
+ rs->common.preferred_currency
+ = GNUNET_strdup (rs->common.countries[i].currency);
+ break;
+ }
+ ANASTASIS_REDUX_return_ (rs,
+ cb,
+ cb_cls,
+ TALER_EC_NONE);
+ return NULL;
+}
+
+
+/**
+ * DispatchHandler/Callback function which is called for a
+ * "select_currency" action.
+ *
+ * Which currency the user would rather pay in is advisory: every order
+ * this provider creates carries all of its currencies as choices, so the
+ * wallet may still settle in another one. What it decides is which total
+ * the reducer quotes and which currency a UI puts in front of the user.
+ *
+ * @param state state to operate on
+ * @param arguments arguments to use for operation on state
+ * @param cb callback to call during/after operation
+ * @param cb_cls callback closure
+ * @return NULL
+ */
+static struct ANASTASIS_ReduxAction *
+select_currency (struct ANASTASIS_ReduxState *rs,
+ const json_t *arguments,
+ ANASTASIS_ActionCallback cb,
+ void *cb_cls)
+{
+ const char *currency;
+
+ if (NULL == arguments)
+ {
+ ANASTASIS_REDUX_fail_ (rs,
+ cb,
+ cb_cls,
+ TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
+ "arguments missing");
+ return NULL;
+ }
+ currency = json_string_value (json_object_get (arguments,
+ "currency"));
+ if (NULL == currency)
+ {
+ ANASTASIS_REDUX_fail_ (rs,
+ cb,
+ cb_cls,
+ TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
+ "'currency' missing");
+ return NULL;
+ }
+ /* Not checked against the providers: at this point their /config
+ requests are still in flight, and a currency none of them offers is
+ not an error either --- it just means no single-currency total can
+ be quoted. */
+ if (GNUNET_OK !=
+ TALER_check_currency (currency))
+ {
+ ANASTASIS_REDUX_fail_ (rs,
+ cb,
+ cb_cls,
+ TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
+ "'currency' malformed");
+ return NULL;
+ }
+ GNUNET_free (rs->common.preferred_currency);
+ rs->common.preferred_currency = GNUNET_strdup (currency);
ANASTASIS_REDUX_return_ (rs,
cb,
cb_cls,
@@ -1984,6 +2145,16 @@ ANASTASIS_redux_action (const json_t *state,
},
{
ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
+ "select_currency",
+ &select_currency
+ },
+ {
+ ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING,
+ "select_currency",
+ &select_currency
+ },
+ {
+ ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
"select_continent",
&select_continent
},
diff --git a/src/reducer/anastasis_api_redux_free.c b/src/reducer/anastasis_api_redux_free.c
@@ -42,10 +42,19 @@ ANASTASIS_REDUX_provider_config_clear_ (struct ANASTASIS_ReduxProviderConfig *
)
{
for (unsigned int j = 0; j < cfg->methods_len; j++)
+ {
GNUNET_free (cfg->methods[j].type);
+ TALER_amount_list_free (&cfg->methods[j].usage_fees);
+ }
GNUNET_free (cfg->methods);
GNUNET_free (cfg->business_name);
GNUNET_free (cfg->currency);
+ TALER_amount_list_free (&cfg->annual_fees);
+ TALER_amount_list_free (&cfg->truth_upload_fees);
+ TALER_amount_list_free (&cfg->liability_limits);
+ for (unsigned int j = 0; j < cfg->currencies_len; j++)
+ GNUNET_free (cfg->currencies[j]);
+ GNUNET_free (cfg->currencies);
memset (cfg,
0,
sizeof (*cfg));
@@ -149,6 +158,7 @@ free_common (struct ANASTASIS_ReduxCommon *common)
json_decref (common->identity_attributes);
GNUNET_free (common->selected_continent);
GNUNET_free (common->selected_country);
+ GNUNET_free (common->preferred_currency);
}
diff --git a/src/reducer/anastasis_api_redux_parse.c b/src/reducer/anastasis_api_redux_parse.c
@@ -488,14 +488,18 @@ parse_provider (const char *url,
"status",
"methods",
"annual_fee",
+ "annual_fees",
"truth_upload_fee",
+ "truth_upload_fees",
"liability_limit",
+ "liability_limits",
"business_name",
"storage_limit_in_megabytes",
"provider_salt",
"http_status",
"error_code",
"currency",
+ "currencies",
NULL
};
const char *status;
@@ -578,6 +582,7 @@ parse_provider (const char *url,
const char *business_name;
const char *currency = NULL;
const json_t *methods;
+ const json_t *currencies = NULL;
bool no_config = false;
struct GNUNET_JSON_Specification cspec[] = {
GNUNET_JSON_spec_array_const ("methods",
@@ -588,6 +593,24 @@ parse_provider (const char *url,
&cfg->truth_upload_fee),
TALER_JSON_spec_amount_any ("liability_limit",
&cfg->liability_limit),
+ /* Optional so that a state serialized by an older reducer still
+ parses; synthesized from the scalars below when absent. */
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("annual_fees",
+ &cfg->annual_fees),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("truth_upload_fees",
+ &cfg->truth_upload_fees),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("liability_limits",
+ &cfg->liability_limits),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("currencies",
+ ¤cies),
+ NULL),
GNUNET_JSON_spec_string ("business_name",
&business_name),
GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes",
@@ -630,6 +653,40 @@ parse_provider (const char *url,
p->have_config = true;
cfg->business_name = GNUNET_strdup (business_name);
cfg->currency = dup_or_null (currency);
+ if (0 == cfg->annual_fees.tal_len)
+ GNUNET_array_append (cfg->annual_fees.tal,
+ cfg->annual_fees.tal_len,
+ cfg->annual_fee);
+ if (0 == cfg->truth_upload_fees.tal_len)
+ GNUNET_array_append (cfg->truth_upload_fees.tal,
+ cfg->truth_upload_fees.tal_len,
+ cfg->truth_upload_fee);
+ if (0 == cfg->liability_limits.tal_len)
+ GNUNET_array_append (cfg->liability_limits.tal,
+ cfg->liability_limits.tal_len,
+ cfg->liability_limit);
+ if (NULL != currencies)
+ {
+ size_t cidx;
+ json_t *cv;
+
+ json_array_foreach (currencies, cidx, cv)
+ {
+ if (! json_is_string (cv))
+ {
+ *detail = set_detail ("authentication_providers",
+ "currencies");
+ return GNUNET_SYSERR;
+ }
+ GNUNET_array_append (cfg->currencies,
+ cfg->currencies_len,
+ GNUNET_strdup (json_string_value (cv)));
+ }
+ }
+ if (0 == cfg->currencies_len)
+ GNUNET_array_append (cfg->currencies,
+ cfg->currencies_len,
+ GNUNET_strdup (cfg->annual_fee.currency));
cfg->methods_len = (unsigned int) json_array_size (methods);
cfg->methods = GNUNET_new_array (cfg->methods_len,
struct ANASTASIS_ReduxMethodSpec);
@@ -638,6 +695,7 @@ parse_provider (const char *url,
static const char *const mallowed[] = {
"type",
"usage_fee",
+ "usage_fees",
NULL
};
struct ANASTASIS_ReduxMethodSpec *ms = &cfg->methods[index];
@@ -647,6 +705,10 @@ parse_provider (const char *url,
&type),
TALER_JSON_spec_amount_any ("usage_fee",
&ms->usage_fee),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("usage_fees",
+ &ms->usage_fees),
+ NULL),
GNUNET_JSON_spec_end ()
};
@@ -666,6 +728,10 @@ parse_provider (const char *url,
return GNUNET_SYSERR;
}
ms->type = GNUNET_strdup (type);
+ if (0 == ms->usage_fees.tal_len)
+ GNUNET_array_append (ms->usage_fees.tal,
+ ms->usage_fees.tal_len,
+ ms->usage_fee);
}
}
return GNUNET_OK;
@@ -736,6 +802,7 @@ parse_common (const json_t *json,
const json_t *currencies = NULL;
const char *selected_continent = NULL;
const char *selected_country = NULL;
+ const char *preferred_currency = NULL;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("continents",
@@ -769,6 +836,10 @@ parse_common (const json_t *json,
GNUNET_JSON_spec_string ("selected_country",
&selected_country),
NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("preferred_currency",
+ &preferred_currency),
+ NULL),
GNUNET_JSON_spec_end ()
};
@@ -783,6 +854,7 @@ parse_common (const json_t *json,
}
common->selected_continent = dup_or_null (selected_continent);
common->selected_country = dup_or_null (selected_country);
+ common->preferred_currency = dup_or_null (preferred_currency);
common->identity_attributes = json_incref ((json_t *) identity_attributes);
if (NULL != currencies)
{
@@ -2068,6 +2140,7 @@ static const char *const backup_allowed[] = {
"currencies",
"selected_continent",
"selected_country",
+ "preferred_currency",
"authentication_methods",
"policies",
"policy_providers",
@@ -2097,6 +2170,7 @@ static const char *const recovery_allowed[] = {
"currencies",
"selected_continent",
"selected_country",
+ "preferred_currency",
"recovery_information",
"recovery_document",
"challenge_feedback",
diff --git a/src/reducer/anastasis_api_redux_serialize.c b/src/reducer/anastasis_api_redux_serialize.c
@@ -269,31 +269,51 @@ serialize_providers (const struct ANASTASIS_ReduxCommon *common)
GNUNET_JSON_pack_string ("type",
ms->type),
TALER_JSON_pack_amount ("usage_fee",
- &ms->usage_fee))));
+ &ms->usage_fee),
+ TALER_JSON_pack_amount_list ("usage_fees",
+ &ms->usage_fees))));
+ }
+ {
+ json_t *currencies = json_array ();
+
+ GNUNET_assert (NULL != currencies);
+ for (unsigned int j = 0; j < cfg->currencies_len; j++)
+ GNUNET_assert (0 ==
+ json_array_append_new (
+ currencies,
+ json_string (cfg->currencies[j])));
+ po = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("status",
+ status),
+ GNUNET_JSON_pack_array_steal ("methods",
+ methods),
+ GNUNET_JSON_pack_array_steal ("currencies",
+ currencies),
+ TALER_JSON_pack_amount ("annual_fee",
+ &cfg->annual_fee),
+ TALER_JSON_pack_amount_list ("annual_fees",
+ &cfg->annual_fees),
+ TALER_JSON_pack_amount ("truth_upload_fee",
+ &cfg->truth_upload_fee),
+ TALER_JSON_pack_amount_list ("truth_upload_fees",
+ &cfg->truth_upload_fees),
+ TALER_JSON_pack_amount ("liability_limit",
+ &cfg->liability_limit),
+ TALER_JSON_pack_amount_list ("liability_limits",
+ &cfg->liability_limits),
+ GNUNET_JSON_pack_string ("business_name",
+ cfg->business_name),
+ GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes",
+ cfg->storage_limit_in_megabytes),
+ GNUNET_JSON_pack_data_auto ("provider_salt",
+ &cfg->provider_salt),
+ GNUNET_JSON_pack_uint64 ("http_status",
+ cfg->http_status),
+ GNUNET_JSON_pack_conditional (
+ NULL != cfg->currency,
+ GNUNET_JSON_pack_string ("currency",
+ cfg->currency)));
}
- po = GNUNET_JSON_PACK (
- GNUNET_JSON_pack_string ("status",
- status),
- GNUNET_JSON_pack_array_steal ("methods",
- methods),
- TALER_JSON_pack_amount ("annual_fee",
- &cfg->annual_fee),
- TALER_JSON_pack_amount ("truth_upload_fee",
- &cfg->truth_upload_fee),
- TALER_JSON_pack_amount ("liability_limit",
- &cfg->liability_limit),
- GNUNET_JSON_pack_string ("business_name",
- cfg->business_name),
- GNUNET_JSON_pack_uint64 ("storage_limit_in_megabytes",
- cfg->storage_limit_in_megabytes),
- GNUNET_JSON_pack_data_auto ("provider_salt",
- &cfg->provider_salt),
- GNUNET_JSON_pack_uint64 ("http_status",
- cfg->http_status),
- GNUNET_JSON_pack_conditional (
- NULL != cfg->currency,
- GNUNET_JSON_pack_string ("currency",
- cfg->currency)));
}
GNUNET_assert (0 ==
json_object_set_new (obj,
@@ -357,6 +377,10 @@ serialize_common (json_t *obj,
set_opt (obj,
"selected_country",
json_string (common->selected_country));
+ if (NULL != common->preferred_currency)
+ set_opt (obj,
+ "preferred_currency",
+ json_string (common->preferred_currency));
}
diff --git a/src/reducer/anastasis_api_redux_state.h b/src/reducer/anastasis_api_redux_state.h
@@ -282,9 +282,17 @@ struct ANASTASIS_ReduxMethodSpec
char *type;
/**
- * Fee charged for using this method during recovery.
+ * Fee charged for using this method during recovery, in the provider's
+ * primary currency.
*/
struct TALER_Amount usage_fee;
+
+ /**
+ * Fee charged for using this method during recovery, one entry per
+ * currency the provider accepts. Never empty: for a provider that
+ * only reports the scalar fee, it holds that one amount.
+ */
+ struct TALER_AmountList usage_fees;
};
@@ -304,21 +312,50 @@ struct ANASTASIS_ReduxProviderConfig
unsigned int methods_len;
/**
- * Fee for storing a policy for a year.
+ * Fee for storing a policy for a year, in the primary currency.
*/
struct TALER_Amount annual_fee;
/**
- * Fee for uploading one truth.
+ * Fee for storing a policy for a year, per currency. Never empty.
+ */
+ struct TALER_AmountList annual_fees;
+
+ /**
+ * Fee for uploading one truth, in the primary currency.
*/
struct TALER_Amount truth_upload_fee;
/**
- * Maximum liability the provider accepts for data loss.
+ * Fee for uploading one truth, per currency. Never empty.
+ */
+ struct TALER_AmountList truth_upload_fees;
+
+ /**
+ * Maximum liability the provider accepts for data loss, in the primary
+ * currency.
*/
struct TALER_Amount liability_limit;
/**
+ * Maximum liability the provider accepts for data loss, per currency.
+ * Never empty.
+ */
+ struct TALER_AmountList liability_limits;
+
+ /**
+ * Currencies the provider accepts, primary currency first. Never
+ * empty; for a provider that predates multi-currency support it holds
+ * the single currency it quotes everything in.
+ */
+ char **currencies;
+
+ /**
+ * Length of the @e currencies array.
+ */
+ unsigned int currencies_len;
+
+ /**
* Salt of this provider.
*/
struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
@@ -330,7 +367,8 @@ struct ANASTASIS_ReduxProviderConfig
/**
* Currency the provider charges in, NULL if not given. Carried
- * through for the benefit of user interfaces.
+ * through for the benefit of user interfaces; @e currencies is the
+ * authoritative list and this is its first entry.
*/
char *currency;
@@ -1199,6 +1237,14 @@ struct ANASTASIS_ReduxCommon
char *selected_country;
/**
+ * Currency the user would rather pay in, NULL if none was chosen.
+ * Advisory: the orders carry every currency the provider accepts as a
+ * choice, and the wallet is free to pick another one. Defaults to the
+ * selected country's currency.
+ */
+ char *preferred_currency;
+
+ /**
* True if the state has a @e continents field at all. An array can
* legitimately be present but empty, which is different from being
* absent, so presence is tracked separately from the length.
@@ -1221,7 +1267,10 @@ struct ANASTASIS_ReduxCommon
bool have_providers;
/**
- * Currencies in play, carried through for user interfaces.
+ * Currencies every provider used by this backup accepts, i.e. the
+ * currencies in which the whole backup can be paid for with a single
+ * choice of currency. May be empty, which is not an error: it means
+ * the user will settle more than one order in more than one currency.
*/
char **currencies;
diff --git a/src/restclient/anastasis_api_config.c b/src/restclient/anastasis_api_config.c
@@ -37,8 +37,13 @@
* string: every key derived by a 0:x:x client differs, so backups written by
* one side cannot be recovered by the other. Hence 0 must stay outside of
* the compatibility range.
+ *
+ * Raised to 3 for multi-currency pricing, which only *added* the plural
+ * fee fields and "currencies" to /config. The age therefore stays at 1:
+ * a version 2 provider is still usable, we just synthesize the plural
+ * fields from the scalar ones below.
*/
-#define ANASTASIS_PROTOCOL_CURRENT 2
+#define ANASTASIS_PROTOCOL_CURRENT 3
/**
* How many versions are we backwards compatible with?
@@ -77,6 +82,256 @@ struct ANASTASIS_ConfigOperation
/**
+ * Parse a successful GET /config response and hand it to @a cb.
+ *
+ * Kept apart from #handle_config_finished() because the price lists are
+ * heap allocated: every exit path from here has to release them, and a
+ * flat sequence of `break's out of a switch cannot express that.
+ *
+ * @param json the response to parse
+ * @param url provider URL, for logging
+ * @param[in,out] acfg configuration to fill in and report
+ * @param cb function to call with the parsed configuration
+ * @param cb_cls closure for @a cb
+ * @return #GNUNET_OK if @a cb was invoked, #GNUNET_SYSERR if @a json was
+ * unusable and the caller must report the failure itself
+ */
+static enum GNUNET_GenericReturnValue
+handle_config_ok (const json_t *json,
+ const char *url,
+ struct ANASTASIS_Config *acfg,
+ ANASTASIS_ConfigCallback cb,
+ void *cb_cls)
+{
+ const char *name;
+ const json_t *methods;
+ const json_t *currencies = NULL;
+ struct TALER_JSON_ProtocolVersion pv;
+ struct ANASTASIS_AuthorizationMethodConfig *mcfg = NULL;
+ const char **carr = NULL;
+ unsigned int carr_len = 0;
+ enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("name",
+ &name),
+ GNUNET_JSON_spec_string ("business_name",
+ &acfg->details.ok.business_name),
+ GNUNET_JSON_spec_string ("version",
+ &acfg->details.ok.version),
+ TALER_JSON_spec_version ("version",
+ &pv),
+ GNUNET_JSON_spec_array_const ("methods",
+ &methods),
+ GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes",
+ &acfg->details.ok.storage_limit_in_megabytes),
+ TALER_JSON_spec_amount_any ("annual_fee",
+ &acfg->details.ok.annual_fee),
+ TALER_JSON_spec_amount_any ("truth_upload_fee",
+ &acfg->details.ok.truth_upload_fee),
+ TALER_JSON_spec_amount_any ("liability_limit",
+ &acfg->details.ok.liability_limit),
+ /* Added in protocol version 3. A version 2 provider omits them, and
+ they are synthesized from the scalar fields further down. */
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("currencies",
+ ¤cies),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("annual_fees",
+ &acfg->details.ok.annual_fees),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("truth_upload_fees",
+ &acfg->details.ok.truth_upload_fees),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("liability_limits",
+ &acfg->details.ok.liability_limits),
+ NULL),
+ GNUNET_JSON_spec_fixed_auto ("provider_salt",
+ &acfg->details.ok.provider_salt),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json,
+ spec,
+ NULL, NULL))
+ {
+ GNUNET_break_op (0);
+ json_dumpf (json,
+ stderr,
+ JSON_INDENT (2));
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ return GNUNET_SYSERR;
+ }
+ if (0 != strcmp (name,
+ "anastasis"))
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+ if ( (ANASTASIS_PROTOCOL_CURRENT < pv.current) &&
+ (ANASTASIS_PROTOCOL_CURRENT < pv.current - pv.age) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Provider protocol version too new\n");
+ acfg->ec = TALER_EC_GENERIC_VERSION_MALFORMED;
+ goto cleanup;
+ }
+ if ( (ANASTASIS_PROTOCOL_CURRENT > pv.current) &&
+ (ANASTASIS_PROTOCOL_CURRENT - ANASTASIS_PROTOCOL_AGE > pv.current) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Provider protocol version too old\n");
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_VERSION_MALFORMED;
+ goto cleanup;
+ }
+ acfg->details.ok.methods_length = (unsigned int) json_array_size (methods);
+ if (((size_t) acfg->details.ok.methods_length) !=
+ json_array_size (methods))
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+ /* Bound the arrays so a malicious provider cannot force an unbounded
+ allocation (a ~40 MB response could otherwise yield ~10^6 elements). */
+ if (acfg->details.ok.methods_length > 1024)
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+
+ /* Currencies: given outright by a version 3 provider, otherwise the
+ single currency the provider was already quoting everything in. */
+ if (NULL != currencies)
+ {
+ size_t clen = json_array_size (currencies);
+ size_t idx;
+ json_t *entry;
+
+ if ( (0 == clen) ||
+ (clen > 1024) )
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+ carr = GNUNET_new_array (clen,
+ const char *);
+ json_array_foreach (currencies, idx, entry)
+ {
+ if (! json_is_string (entry))
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+ carr[idx] = json_string_value (entry);
+ carr_len = idx + 1;
+ }
+ }
+ else
+ {
+ carr = GNUNET_new_array (1,
+ const char *);
+ carr[0] = acfg->details.ok.annual_fee.currency;
+ carr_len = 1;
+ }
+ acfg->details.ok.currencies = carr;
+ acfg->details.ok.currencies_len = carr_len;
+
+ /* Plural fee fields: a version 2 provider has none, so make a
+ one-element list out of what it did send. Everything downstream can
+ then work with lists only. */
+ {
+ struct
+ {
+ const struct TALER_Amount *scalar;
+ struct TALER_AmountList *list;
+ } fees[] = {
+ { &acfg->details.ok.annual_fee, &acfg->details.ok.annual_fees },
+ { &acfg->details.ok.truth_upload_fee,
+ &acfg->details.ok.truth_upload_fees },
+ { &acfg->details.ok.liability_limit,
+ &acfg->details.ok.liability_limits }
+ };
+
+ for (unsigned int i = 0; i<sizeof (fees) / sizeof (fees[0]); i++)
+ {
+ if (0 != fees[i].list->tal_len)
+ continue;
+ GNUNET_array_append (fees[i].list->tal,
+ fees[i].list->tal_len,
+ *fees[i].scalar);
+ }
+ }
+
+ mcfg = GNUNET_new_array (GNUNET_NZL (acfg->details.ok.methods_length),
+ struct ANASTASIS_AuthorizationMethodConfig);
+ for (unsigned int i = 0; i<acfg->details.ok.methods_length; i++)
+ {
+ struct ANASTASIS_AuthorizationMethodConfig *m = &mcfg[i];
+ struct GNUNET_JSON_Specification ispec[] = {
+ GNUNET_JSON_spec_string ("type",
+ &m->type),
+ TALER_JSON_spec_amount_any ("cost",
+ &m->usage_fee),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_list ("costs",
+ &m->usage_fees),
+ NULL),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json_array_get (methods,
+ i),
+ ispec,
+ NULL, NULL))
+ {
+ GNUNET_break_op (0);
+ acfg->ec = TALER_EC_GENERIC_REPLY_MALFORMED;
+ goto cleanup;
+ }
+ if (0 == m->usage_fees.tal_len)
+ GNUNET_array_append (m->usage_fees.tal,
+ m->usage_fees.tal_len,
+ m->usage_fee);
+ }
+ acfg->details.ok.methods = mcfg;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Good backend found at `%s'\n",
+ url);
+ cb (cb_cls,
+ acfg);
+ ret = GNUNET_OK;
+cleanup:
+ if (NULL != mcfg)
+ {
+ for (unsigned int i = 0; i<acfg->details.ok.methods_length; i++)
+ TALER_amount_list_free (&mcfg[i].usage_fees);
+ GNUNET_free (mcfg);
+ }
+ GNUNET_free (carr);
+ GNUNET_JSON_parse_free (spec);
+ if (GNUNET_OK != ret)
+ {
+ /* Everything in there points into memory just released, and the
+ caller is about to report a failure with the same struct. */
+ memset (&acfg->details.ok,
+ 0,
+ sizeof (acfg->details.ok));
+ }
+ return ret;
+}
+
+
+/**
* Function called when we're done processing the
* HTTP /config request.
*
@@ -106,129 +361,18 @@ handle_config_finished (void *cls,
co->url);
break;
case MHD_HTTP_OK:
+ if (GNUNET_OK ==
+ handle_config_ok (json,
+ co->url,
+ &acfg,
+ co->cb,
+ co->cb_cls))
{
- const char *name;
- const json_t *methods;
- struct TALER_JSON_ProtocolVersion pv;
- struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_string ("name",
- &name),
- GNUNET_JSON_spec_string ("business_name",
- &acfg.details.ok.business_name),
- GNUNET_JSON_spec_string ("version",
- &acfg.details.ok.version),
- TALER_JSON_spec_version ("version",
- &pv),
- GNUNET_JSON_spec_array_const ("methods",
- &methods),
- GNUNET_JSON_spec_uint32 ("storage_limit_in_megabytes",
- &acfg.details.ok.storage_limit_in_megabytes),
- TALER_JSON_spec_amount_any ("annual_fee",
- &acfg.details.ok.annual_fee),
- TALER_JSON_spec_amount_any ("truth_upload_fee",
- &acfg.details.ok.truth_upload_fee),
- TALER_JSON_spec_amount_any ("liability_limit",
- &acfg.details.ok.liability_limit),
- GNUNET_JSON_spec_fixed_auto ("provider_salt",
- &acfg.details.ok.provider_salt),
- GNUNET_JSON_spec_end ()
- };
-
- if (GNUNET_OK !=
- GNUNET_JSON_parse (json,
- spec,
- NULL, NULL))
- {
- GNUNET_break_op (0);
- json_dumpf (json,
- stderr,
- JSON_INDENT (2));
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
- break;
- }
- if (0 != strcmp (name,
- "anastasis"))
- {
- GNUNET_JSON_parse_free (spec);
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
- break;
- }
- if ( (ANASTASIS_PROTOCOL_CURRENT < pv.current) &&
- (ANASTASIS_PROTOCOL_CURRENT < pv.current - pv.age) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Provider protocol version too new\n");
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_VERSION_MALFORMED;
- break;
- }
- if ( (ANASTASIS_PROTOCOL_CURRENT > pv.current) &&
- (ANASTASIS_PROTOCOL_CURRENT - ANASTASIS_PROTOCOL_AGE > pv.current) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Provider protocol version too old\n");
- GNUNET_break_op (0);
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_VERSION_MALFORMED;
- break;
- }
- acfg.details.ok.methods_length = (unsigned int) json_array_size (methods);
- if (((size_t) acfg.details.ok.methods_length) !=
- json_array_size (methods))
- {
- GNUNET_break_op (0);
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
- break;
- }
- /* Bound the array so a malicious provider cannot force an unbounded
- stack VLA (a ~40 MB response could otherwise yield ~10^6 elements). */
- if (acfg.details.ok.methods_length > 1024)
- {
- GNUNET_break_op (0);
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
- break;
- }
- {
- struct ANASTASIS_AuthorizationMethodConfig mcfg[
- GNUNET_NZL (acfg.details.ok.methods_length)];
-
- for (unsigned int i = 0; i<acfg.details.ok.methods_length; i++)
- {
- struct ANASTASIS_AuthorizationMethodConfig *m = &mcfg[i];
- struct GNUNET_JSON_Specification ispec[] = {
- GNUNET_JSON_spec_string ("type",
- &m->type),
- TALER_JSON_spec_amount_any ("cost",
- &m->usage_fee),
- GNUNET_JSON_spec_end ()
- };
-
- if ( (GNUNET_OK !=
- GNUNET_JSON_parse (json_array_get (methods,
- i),
- ispec,
- NULL, NULL)) )
- {
- GNUNET_break_op (0);
- acfg.http_status = 0;
- acfg.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
- goto end;
- }
- }
- acfg.details.ok.methods = mcfg;
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Good backend found at `%s'\n",
- co->url);
- co->cb (co->cb_cls,
- &acfg);
- ANASTASIS_config_cancel (co);
- return;
- }
+ ANASTASIS_config_cancel (co);
+ return;
}
+ acfg.http_status = 0;
+ break;
case MHD_HTTP_BAD_REQUEST:
/* This should never happen, either us or the anastasis server is buggy
(or API version conflict); just pass JSON reply to the application */
@@ -248,7 +392,6 @@ handle_config_finished (void *cls,
GNUNET_break_op (0);
break;
}
-end:
co->cb (co->cb_cls,
&acfg);
ANASTASIS_config_cancel (co);
diff --git a/src/stasis/anastasis-db_pg.h b/src/stasis/anastasis-db_pg.h
@@ -65,7 +65,11 @@ struct PostgresClosure
const char *transaction_name;
/**
- * Currency we accept payments in.
+ * Currency of the provider's own bank account, from [anastasis] CURRENCY.
+ * This is *not* the currency the service is priced in: since amounts in
+ * the database carry their own currency and a provider may price in
+ * several at once, this is only used to interpret IBAN wire transfers,
+ * which are made into one bank account and are therefore in one currency.
*/
char *currency;
diff --git a/src/stasis/anastasis-dbinit.c b/src/stasis/anastasis-dbinit.c
@@ -77,6 +77,26 @@ run (void *cls,
ANASTASIS_DB_fini ();
return;
}
+ /* Finish what the stasis-0004 migration could not: SQL has no access to
+ [anastasis] CURRENCY, so the IBAN transfers it widened were left with a
+ NULL currency. Idempotent, and a no-op for a freshly created database. */
+ {
+ enum GNUNET_DB_QueryStatus qs;
+
+ qs = ANASTASIS_DB_update_auth_iban_in_currency ();
+ if (0 > qs)
+ {
+ fprintf (stderr,
+ "Failed to set the currency of migrated IBAN transfers.\n");
+ global_ret = EXIT_FAILURE;
+ ANASTASIS_DB_fini ();
+ return;
+ }
+ if (0 < qs)
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Stamped currency onto %d migrated IBAN transfer(s)\n",
+ (int) qs);
+ }
if (gc_db)
{
struct GNUNET_TIME_Absolute expire_backups;
diff --git a/src/stasis/do_insert_recdoc_payment.c b/src/stasis/do_insert_recdoc_payment.c
@@ -38,8 +38,8 @@ ANASTASIS_DB_do_insert_recdoc_payment (
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (account_pub),
GNUNET_PQ_query_param_uint32 (&post_counter),
- TALER_PQ_query_param_amount (pg->conn,
- amount),
+ TALER_PQ_query_param_amount_with_currency (pg->conn,
+ amount),
GNUNET_PQ_query_param_auto_from_type (payment_secret),
GNUNET_PQ_query_param_timestamp (&now),
GNUNET_PQ_query_param_timestamp (&transient_expiration),
diff --git a/src/stasis/do_insert_recdoc_payment.sql b/src/stasis/do_insert_recdoc_payment.sql
@@ -18,7 +18,7 @@ DROP FUNCTION IF EXISTS anastasis_do_insert_recdoc_payment;
CREATE FUNCTION anastasis_do_insert_recdoc_payment (
IN in_user_id BYTEA,
IN in_post_counter INT4,
- IN in_amount taler_amount,
+ IN in_amount taler_amount_currency,
IN in_payment_identifier BYTEA,
IN in_creation_date INT8,
IN in_transient_expiration INT8) -- lifetime given to an account created here
diff --git a/src/stasis/get_challenge_payment.c b/src/stasis/get_challenge_payment.c
@@ -33,13 +33,17 @@
* @param payment_secret payment secret which the user must provide with every upload
* @param truth_uuid which truth should we check the payment status of
* @param[out] paid bool value to show if payment is paid
+ * @param[out] amount set to the amount that was paid, in the currency it was
+ * paid in; only meaningful if @a paid is true, as the row still
+ * carries the asking price until the payment is confirmed
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_get_challenge_payment (
const struct ANASTASIS_PaymentSecretP *payment_secret,
const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
- bool *paid)
+ bool *paid,
+ struct TALER_Amount *amount)
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (payment_secret),
@@ -49,12 +53,18 @@ ANASTASIS_DB_get_challenge_payment (
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_bool ("paid",
paid),
+ TALER_PQ_result_spec_amount_with_currency ("amount",
+ amount),
GNUNET_PQ_result_spec_end
};
+ /* The amount is read back because a refund has to return what was
+ actually paid: with several currencies on offer the configured price
+ no longer identifies it. */
PREPARE ("get_challenge_payment",
"SELECT"
" paid"
+ " ,amount"
" FROM anastasis_challenge_payment"
" WHERE payment_identifier=$1"
" AND truth_uuid=$2"
diff --git a/src/stasis/get_pending_challenge_payment.c b/src/stasis/get_pending_challenge_payment.c
@@ -58,7 +58,6 @@ ANASTASIS_DB_get_pending_challenge_payment (
"SELECT"
" creation_date"
",payment_identifier"
- ",amount"
" FROM anastasis_challenge_payment"
" WHERE"
" paid=FALSE"
diff --git a/src/stasis/get_recdoc_payment.c b/src/stasis/get_recdoc_payment.c
@@ -70,7 +70,6 @@ ANASTASIS_DB_get_recdoc_payment (
"SELECT"
" creation_date"
",post_counter > 0 AS valid_counter"
- ",amount"
",paid"
" FROM anastasis_recdoc_payment"
" WHERE user_id=$1"
diff --git a/src/stasis/insert_auth_iban_in.c b/src/stasis/insert_auth_iban_in.c
@@ -49,8 +49,8 @@ ANASTASIS_DB_insert_auth_iban_in (
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_uint64 (&wire_reference),
GNUNET_PQ_query_param_string (wire_subject),
- TALER_PQ_query_param_amount (pg->conn,
- amount),
+ TALER_PQ_query_param_amount_with_currency (pg->conn,
+ amount),
GNUNET_PQ_query_param_string (debit_account),
GNUNET_PQ_query_param_string (credit_account),
GNUNET_PQ_query_param_timestamp (&execution_date),
diff --git a/src/stasis/insert_challenge_payment.c b/src/stasis/insert_challenge_payment.c
@@ -43,8 +43,8 @@ ANASTASIS_DB_insert_challenge_payment (
struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (truth_uuid),
- TALER_PQ_query_param_amount (pg->conn,
- amount),
+ TALER_PQ_query_param_amount_with_currency (pg->conn,
+ amount),
GNUNET_PQ_query_param_auto_from_type (payment_secret),
GNUNET_PQ_query_param_timestamp (&now),
GNUNET_PQ_query_param_end
diff --git a/src/stasis/insert_truth_payment.c b/src/stasis/insert_truth_payment.c
@@ -44,8 +44,8 @@ ANASTASIS_DB_insert_truth_payment (
duration);
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (uuid),
- TALER_PQ_query_param_amount (pg->conn,
- amount),
+ TALER_PQ_query_param_amount_with_currency (pg->conn,
+ amount),
GNUNET_PQ_query_param_timestamp (&exp),
GNUNET_PQ_query_param_end
};
diff --git a/src/stasis/iterate_auth_iban_transfers.c b/src/stasis/iterate_auth_iban_transfers.c
@@ -67,9 +67,8 @@ test_auth_cb (void *cls,
struct TALER_Amount credit;
char *wire_subject;
struct GNUNET_PQ_ResultSpec rs[] = {
- TALER_PQ_result_spec_amount ("credit",
- pg->currency,
- &credit),
+ TALER_PQ_result_spec_amount_with_currency ("credit",
+ &credit),
GNUNET_PQ_result_spec_string ("wire_subject",
&wire_subject),
GNUNET_PQ_result_spec_end
diff --git a/src/stasis/meson.build b/src/stasis/meson.build
@@ -8,6 +8,7 @@ install_data(
'stasis-0001.sql',
'stasis-0002.sql',
'stasis-0003.sql',
+ 'stasis-0004.sql',
'drop.sql',
install_dir: sqldir,
)
@@ -55,6 +56,7 @@ libanastasisdb_SOURCES = [
'get_pending_challenge_payment.c',
'update_to_challenge_code_satisfied.c',
'update_totp_counter.c',
+ 'update_auth_iban_in_currency.c',
'update_to_challenge_code_sent.c',
'insert_auth_iban_in.c',
'insert_challenge_payment.c',
diff --git a/src/stasis/stasis-0004.sql b/src/stasis/stasis-0004.sql
@@ -0,0 +1,89 @@
+--
+-- This file is part of Anastasis
+-- Copyright (C) 2026 Anastasis SARL
+--
+-- ANASTASIS 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.
+--
+-- ANASTASIS 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
+-- ANASTASIS; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+--
+
+-- Everything in one big transaction
+BEGIN;
+
+-- Check patch versioning is in place.
+SELECT _v.register_patch('stasis-0004', NULL, NULL);
+
+SET search_path TO anastasis;
+
+-- A provider may now price its service in several currencies at once, so an
+-- amount is only meaningful together with the currency it is in. The old
+-- taler_amount type has no room for one: it was written on the assumption
+-- that the whole provider used exactly one currency, which was then read out
+-- of [anastasis] CURRENCY at result-parsing time. Replace it with the
+-- merchant's taler_amount_currency, which carries the currency in the row.
+--
+-- anastasis_do_insert_recdoc_payment() names taler_amount in its signature
+-- and therefore depends on the type. It has to be dropped before the type
+-- can be, and dropping it here costs nothing: every anastasis-dbinit run
+-- recreates all stored procedures from procedures.sql. Doing it explicitly
+-- rather than with DROP TYPE ... CASCADE keeps the dependency visible, and
+-- makes an unexpected second dependent an error instead of a silent drop.
+DROP FUNCTION IF EXISTS anastasis_do_insert_recdoc_payment;
+
+CREATE TYPE taler_amount_currency
+ AS
+ (val INT8
+ ,frac INT4
+ ,curr VARCHAR(12)
+ );
+COMMENT ON TYPE taler_amount_currency
+ IS 'Stores an amount with its currency, fraction is in units of 1/100000000 of the base value';
+
+-- Existing payment rows were all made in the provider's single configured
+-- currency, but that currency is in the configuration file and not reachable
+-- from here. Stamp them KUDOS: these three columns have never been read back
+-- by any released version (the getters either did not select the column or
+-- selected it without binding a result), so no decision depends on the value
+-- being right, and the alternative -- a NULL currency -- would make them
+-- unparseable rather than merely historical.
+ALTER TABLE anastasis_truth_payment
+ ALTER COLUMN amount TYPE taler_amount_currency
+ USING ROW((amount).val, (amount).frac, 'KUDOS')::taler_amount_currency;
+COMMENT ON COLUMN anastasis_truth_payment.amount
+ IS 'Amount we were paid, in the currency the user chose to pay in. Rows migrated from before multi-currency support read KUDOS regardless of what was actually paid.';
+
+ALTER TABLE anastasis_recdoc_payment
+ ALTER COLUMN amount TYPE taler_amount_currency
+ USING ROW((amount).val, (amount).frac, 'KUDOS')::taler_amount_currency;
+COMMENT ON COLUMN anastasis_recdoc_payment.amount
+ IS 'Amount we were paid, in the currency the user chose to pay in. Rows migrated from before multi-currency support read KUDOS regardless of what was actually paid.';
+
+ALTER TABLE anastasis_challenge_payment
+ ALTER COLUMN amount TYPE taler_amount_currency
+ USING ROW((amount).val, (amount).frac, 'KUDOS')::taler_amount_currency;
+COMMENT ON COLUMN anastasis_challenge_payment.amount
+ IS 'Amount we were paid, in the currency the user chose to pay in. This is what a refund of the challenge returns, so it must be the currency actually received. Rows migrated from before multi-currency support read KUDOS regardless of what was actually paid.';
+
+-- The IBAN column is different: it *is* read back, by
+-- ANASTASIS_DB_iterate_auth_iban_transfers(), and the IBAN plugin compares
+-- what it finds against the expected transfer amount. Stamping it KUDOS
+-- would make every pre-upgrade transfer fail that comparison and silently
+-- invalidate all in-flight IBAN challenges. Leave the currency NULL here;
+-- anastasis-dbinit fills it in from [anastasis] CURRENCY, which is the one
+-- place that knows the bank account's currency.
+ALTER TABLE anastasis_auth_iban_in
+ ALTER COLUMN credit TYPE taler_amount_currency
+ USING ROW((credit).val, (credit).frac, NULL)::taler_amount_currency;
+COMMENT ON COLUMN anastasis_auth_iban_in.credit
+ IS 'Amount we were credited, in the currency of the provider bank account ([anastasis] CURRENCY)';
+
+DROP TYPE taler_amount;
+
+COMMIT;
diff --git a/src/stasis/test_anastasis_db.c b/src/stasis/test_anastasis_db.c
@@ -227,26 +227,45 @@ run (void *cls)
&valid_counter,
&pi_creation_date));
}
- FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
- ANASTASIS_DB_get_challenge_payment (
- &paymentSecretP,
- &truth_uuid,
- &paid));
- FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
- ANASTASIS_DB_insert_challenge_payment (
- &truth_uuid,
- &paymentSecretP,
- &amount));
- FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
- ANASTASIS_DB_update_to_challenge_payment_paid (
- &truth_uuid,
- &paymentSecretP));
- FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
- ANASTASIS_DB_get_challenge_payment (
- &paymentSecretP,
- &truth_uuid,
- &paid));
- FAILIF (! paid);
+ {
+ struct TALER_Amount paid_amount;
+ struct TALER_Amount got_amount;
+
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ ANASTASIS_DB_get_challenge_payment (
+ &paymentSecretP,
+ &truth_uuid,
+ &paid,
+ &got_amount));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ ANASTASIS_DB_insert_challenge_payment (
+ &truth_uuid,
+ &paymentSecretP,
+ &amount));
+ /* The wallet settles in a currency of its choosing, which need not be
+ the one the order was quoted in. What comes back out has to be
+ that one, or a refund would be issued in the wrong currency. */
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount ("CHF:23",
+ &paid_amount));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ ANASTASIS_DB_update_to_challenge_payment_paid (
+ &truth_uuid,
+ &paymentSecretP,
+ &paid_amount));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ ANASTASIS_DB_get_challenge_payment (
+ &paymentSecretP,
+ &truth_uuid,
+ &paid,
+ &got_amount));
+ FAILIF (! paid);
+ FAILIF (GNUNET_OK !=
+ TALER_amount_cmp_currency (&paid_amount,
+ &got_amount));
+ FAILIF (0 != TALER_amount_cmp (&paid_amount,
+ &got_amount));
+ }
FAILIF (ANASTASIS_DB_STORE_STATUS_SUCCESS !=
ANASTASIS_DB_do_insert_recovery_document (
&accountPubP,
diff --git a/src/stasis/update_auth_iban_in_currency.c b/src/stasis/update_auth_iban_in_currency.c
@@ -0,0 +1,66 @@
+/*
+ This file is part of Anastasis
+ Copyright (C) 2026 Anastasis SARL
+
+ Anastasis 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.
+
+ Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ Anastasis; see the file COPYING.GPL. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file stasis/update_auth_iban_in_currency.c
+ * @brief Anastasis database: stamp the configured currency onto IBAN
+ * transfers migrated from before amounts carried one
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "anastasis-db_pg.h"
+#include "anastasis/anastasis-database/update_auth_iban_in_currency.h"
+#include "anastasis/anastasis-database/transaction.h"
+#include "anastasis/anastasis-database/preflight.h"
+#include <taler/taler_pq_lib.h>
+
+
+/**
+ * Fill in the currency of IBAN wire transfers recorded before amounts
+ * carried one.
+ *
+ * The stasis-0004 migration widened the amount columns to include a
+ * currency, but SQL cannot know which one: it lives in [anastasis]
+ * CURRENCY. The rows are therefore left with a NULL currency for this
+ * function to complete. Unlike the three payment tables, these rows
+ * cannot simply be stamped with a placeholder --- the IBAN plugin
+ * compares them against the amount it expects, so a wrong currency would
+ * silently invalidate every challenge still in flight across the upgrade.
+ *
+ * @return transaction status; #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if
+ * there was nothing left to fill in, which is the normal case
+ */
+enum GNUNET_DB_QueryStatus
+ANASTASIS_DB_update_auth_iban_in_currency (void)
+{
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_string (pg->currency),
+ GNUNET_PQ_query_param_end
+ };
+
+ /* Idempotent: the WHERE clause matches nothing once the backfill has
+ run, so re-running anastasis-dbinit is free, and a provider that
+ changes CURRENCY later does not have its history rewritten. */
+ PREPARE ("update_auth_iban_in_currency",
+ "UPDATE anastasis_auth_iban_in"
+ " SET credit=ROW((credit).val,(credit).frac,$1)::taler_amount_currency"
+ " WHERE (credit).curr IS NULL;");
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "update_auth_iban_in_currency",
+ params);
+}
+
+
+/* end of update_auth_iban_in_currency.c */
diff --git a/src/stasis/update_to_challenge_payment_paid.c b/src/stasis/update_to_challenge_payment_paid.c
@@ -31,23 +31,32 @@
*
* @param truth_uuid which challenge received a payment
* @param payment_identifier proof of payment, must be unique and match pending payment
+ * @param amount what was actually paid, in the currency it was paid in
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_update_to_challenge_payment_paid (
const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
- const struct ANASTASIS_PaymentSecretP *payment_identifier)
+ const struct ANASTASIS_PaymentSecretP *payment_identifier,
+ const struct TALER_Amount *amount)
{
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (payment_identifier),
GNUNET_PQ_query_param_auto_from_type (truth_uuid),
+ TALER_PQ_query_param_amount_with_currency (pg->conn,
+ amount),
GNUNET_PQ_query_param_end
};
+ /* Until now the row held the asking price in the provider's primary
+ currency, because at order creation time it was not yet known which
+ of the offered currencies the wallet would settle in. Now it is, and
+ a later refund must return that one. */
PREPARE ("update_to_challenge_payment_paid",
"UPDATE anastasis_challenge_payment "
"SET"
- " paid=TRUE "
+ " paid=TRUE"
+ ",amount=$3 "
"WHERE"
" payment_identifier=$1"
" AND"
diff --git a/src/testing/test_anastasis.c b/src/testing/test_anastasis.c
@@ -220,14 +220,19 @@ run (void *cls,
"secret-share-1",
NULL),
/* make the payment */
- TALER_TESTING_cmd_merchant_pay_order ("pay-account",
- merchant_url,
- MHD_HTTP_OK,
- "fetch-proposal",
- "withdraw-coin-1",
- "EUR:5",
- "EUR:4.99", /* must match ANNUAL_FEE in config! */
- NULL),
+ TALER_TESTING_cmd_merchant_pay_order_choices (
+ "pay-account",
+ merchant_url,
+ MHD_HTTP_OK,
+ "fetch-proposal",
+ "withdraw-coin-1",
+ "EUR:5",
+ "EUR:4.99", /* must match ANNUAL_FEE in config! */
+ NULL,
+ /* Anastasis creates "v1" orders now; every configured currency
+ is a choice and the first one is the primary currency. */
+ 0,
+ NULL),
ANASTASIS_TESTING_cmd_secret_share ("secret-share-2",
anastasis_url,
"salt-request-1",
@@ -274,14 +279,19 @@ run (void *cls,
MHD_HTTP_OK,
"challenge-start-3-pay",
NULL),
- TALER_TESTING_cmd_merchant_pay_order ("pay-file-challenge",
- merchant_url,
- MHD_HTTP_OK,
- "fetch-challenge-pay-proposal",
- "withdraw-coin-2",
- "EUR:1",
- "EUR:1", /* must match COST in config! */
- NULL),
+ TALER_TESTING_cmd_merchant_pay_order_choices (
+ "pay-file-challenge",
+ merchant_url,
+ MHD_HTTP_OK,
+ "fetch-challenge-pay-proposal",
+ "withdraw-coin-2",
+ "EUR:1",
+ "EUR:1", /* must match COST in config! */
+ NULL,
+ /* Anastasis creates "v1" orders now; every configured currency
+ is a choice and the first one is the primary currency. */
+ 0,
+ NULL),
ANASTASIS_TESTING_cmd_challenge_start ("challenge-start-3-paid",
"challenge-start-3-pay", /* payment ref */
"recover-secret-1",
diff --git a/src/testing/test_anastasis_api.c b/src/testing/test_anastasis_api.c
@@ -153,14 +153,19 @@ run (void *cls,
"policy-store-1",
NULL),
/* make the payment */
- TALER_TESTING_cmd_merchant_pay_order ("pay-account",
- merchant_url,
- MHD_HTTP_OK,
- "fetch-proposal",
- "withdraw-coin-1",
- "EUR:5",
- "EUR:4.99", /* must match ANNUAL_FEE in config! */
- NULL),
+ TALER_TESTING_cmd_merchant_pay_order_choices (
+ "pay-account",
+ merchant_url,
+ MHD_HTTP_OK,
+ "fetch-proposal",
+ "withdraw-coin-1",
+ "EUR:5",
+ "EUR:4.99", /* must match ANNUAL_FEE in config! */
+ NULL,
+ /* Anastasis creates "v1" orders now; every configured currency
+ is a choice and the first one is the primary currency. */
+ 0,
+ NULL),
ANASTASIS_TESTING_cmd_policy_store ("policy-store-2",
anastasis_url,
"policy-store-1",
@@ -222,14 +227,19 @@ run (void *cls,
"file-challenge-run-1",
NULL),
/* make the payment */
- TALER_TESTING_cmd_merchant_pay_order ("pay-account-2",
- merchant_url,
- MHD_HTTP_OK,
- "fetch-proposal-2",
- "withdraw-coin-2",
- "EUR:1.01",
- "EUR:1",
- NULL),
+ TALER_TESTING_cmd_merchant_pay_order_choices (
+ "pay-account-2",
+ merchant_url,
+ MHD_HTTP_OK,
+ "fetch-proposal-2",
+ "withdraw-coin-2",
+ "EUR:1.01",
+ "EUR:1",
+ NULL,
+ /* Anastasis creates "v1" orders now; every configured currency
+ is a choice and the first one is the primary currency. */
+ 0,
+ NULL),
ANASTASIS_TESTING_cmd_truth_challenge (
"file-challenge-run-2",