gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: wallet: improve retry handlin


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet: improve retry handling for payments, update error codes
Date: Tue, 08 Mar 2022 23:09:25 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 6ee03549 wallet: improve retry handling for payments, update error 
codes
6ee03549 is described below

commit 6ee0354940c09d1065c3b3b7bf08e41fd6014268
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Mar 8 23:09:20 2022 +0100

    wallet: improve retry handling for payments, update error codes
---
 packages/taler-util/src/taler-error-codes.ts       | 431 ++++++++++++--
 packages/taler-util/src/talerTypes.ts              |   5 -
 packages/taler-util/src/walletTypes.ts             |   2 +-
 packages/taler-wallet-core/src/db.ts               |  14 +-
 .../taler-wallet-core/src/operations/README.md     |  21 +-
 .../src/operations/backup/export.ts                |  12 +-
 .../src/operations/backup/import.ts                |  10 +-
 .../taler-wallet-core/src/operations/deposits.ts   |   3 -
 packages/taler-wallet-core/src/operations/pay.ts   | 646 +++++++++++----------
 .../taler-wallet-core/src/operations/pending.ts    |   4 +-
 .../taler-wallet-core/src/operations/refund.ts     |  34 +-
 11 files changed, 783 insertions(+), 399 deletions(-)

diff --git a/packages/taler-util/src/taler-error-codes.ts 
b/packages/taler-util/src/taler-error-codes.ts
index fec2cf0f..b22f29a1 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -36,6 +36,13 @@ export enum TalerErrorCode {
    */
   INVALID = 1,
 
+  /**
+   * An internal failure happened on the client side.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  GENERIC_CLIENT_INTERNAL_ERROR = 2,
+
   /**
    * The response we got from the server was not even in JSON format.
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -92,6 +99,13 @@ export enum TalerErrorCode {
    */
   GENERIC_JSON_INVALID = 22,
 
+  /**
+   * Some of the HTTP headers provided by the client caused the server to not 
be able to handle the request.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  GENERIC_HTTP_HEADERS_MALFORMED = 23,
+
   /**
    * The payto:// URI provided by the client is malformed.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -218,6 +232,13 @@ export enum TalerErrorCode {
    */
   GENERIC_JSON_ALLOCATION_FAILURE = 72,
 
+  /**
+   * The HTTP server failed to allocate memory for making a CURL request.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  GENERIC_CURL_ALLOCATION_FAILURE = 73,
+
   /**
    * Exchange is badly configured and thus cannot operate.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
@@ -269,7 +290,7 @@ export enum TalerErrorCode {
 
   /**
    * The exchange failed to perform the operation as it could not find the 
private keys. This is a problem with the exchange setup, not with the client's 
request.
-   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
    * (A value of 0 indicates that the error is generated client-side).
    */
   EXCHANGE_GENERIC_KEYS_MISSING = 1007,
@@ -295,6 +316,62 @@ export enum TalerErrorCode {
    */
   EXCHANGE_GENERIC_DENOMINATION_REVOKED = 1010,
 
+  /**
+   * An operation where the exchange interacted with a security module timed 
out.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_SECMOD_TIMEOUT = 1011,
+
+  /**
+   * The respective coin did not have sufficient residual value for the 
operation.  The "history" in this response provides the "residual_value" of the 
coin, which may be less than its "original_value".
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_INSUFFICIENT_FUNDS = 1012,
+
+  /**
+   * The exchange had an internal error reconstructing the transaction history 
of the coin that was being processed.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED = 1013,
+
+  /**
+   * The exchange failed to obtain the transaction history of the given coin 
from the database while generating an insufficient funds errors.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1014,
+
+  /**
+   * The same coin was already used with a different age hash previously.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH = 1015,
+
+  /**
+   * The requested operation is not valid for the cipher used by the selected 
denomination.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION = 1016,
+
+  /**
+   * The provided arguments for the operation use inconsistent ciphers.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_CIPHER_MISMATCH = 1017,
+
+  /**
+   * The number of denominations specified in the request exceeds the limit of 
the exchange.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
+
   /**
    * The exchange did not find information about the specified transaction in 
the database.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -401,18 +478,18 @@ export enum TalerErrorCode {
   EXCHANGE_WITHDRAW_UNBLIND_FAILURE = 1159,
 
   /**
-   * The respective coin did not have sufficient residual value for the 
/deposit operation (i.e. due to double spending). The "history" in the response 
provides the transaction history of the coin proving this fact.
-   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * The signature made by the coin over the deposit permission is not valid.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
    * (A value of 0 indicates that the error is generated client-side).
    */
-  EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS = 1200,
+  EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
 
   /**
-   * The signature made by the coin over the deposit permission is not valid.
-   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * The same coin was already deposited for the same merchant and contract 
with other details.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
    * (A value of 0 indicates that the error is generated client-side).
    */
-  EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+  EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT = 1206,
 
   /**
    * The stated value of the coin after the deposit fee is subtracted would be 
negative.
@@ -428,6 +505,13 @@ export enum TalerErrorCode {
    */
   EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
 
+  /**
+   * The stated wire deadline is "never", which makes no sense.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER = 1209,
+
   /**
    * The exchange failed to canonicalize and hash the given wire format. For 
example, the merchant failed to provide the "salt" or a valid payto:// URI in 
the wire details.  Note that while the exchange will do some basic sanity 
checking on the wire details, it cannot warrant that the banking system will 
ultimately be able to route to the specified address, even if this check passed.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -450,25 +534,18 @@ export enum TalerErrorCode {
   EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
 
   /**
-   * The reserve status was requested using a unknown key.
-   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
-   * (A value of 0 indicates that the error is generated client-side).
-   */
-  EXCHANGE_RESERVES_GET_STATUS_UNKNOWN = 1250,
-
-  /**
-   * The respective coin did not have sufficient residual value for the 
/refresh/melt operation.  The "history" in this response provdes the 
"residual_value" of the coin, which may be less than its "original_value".
-   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * The deposited amount is smaller than the deposit fee, which would result 
in a negative contribution.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
-  EXCHANGE_MELT_INSUFFICIENT_FUNDS = 1300,
+  EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
 
   /**
-   * The exchange had an internal error reconstructing the transaction history 
of the coin that was being melted.
-   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * The reserve status was requested using a unknown key.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
    * (A value of 0 indicates that the error is generated client-side).
    */
-  EXCHANGE_MELT_COIN_HISTORY_COMPUTATION_FAILED = 1301,
+  EXCHANGE_RESERVES_GET_STATUS_UNKNOWN = 1250,
 
   /**
    * The exchange encountered melt fees exceeding the melted coin's 
contribution.
@@ -484,13 +561,6 @@ export enum TalerErrorCode {
    */
   EXCHANGE_MELT_COIN_SIGNATURE_INVALID = 1303,
 
-  /**
-   * The exchange failed to obtain the transaction history of the given coin 
from the database while generating an insufficient funds errors.
-   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
-   * (A value of 0 indicates that the error is generated client-side).
-   */
-  EXCHANGE_MELT_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1304,
-
   /**
    * The denomination of the given coin has past its expiration date and it is 
also not a valid zombie (that is, was not refreshed with the fresh coin being 
subjected to recoup).
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -533,13 +603,6 @@ export enum TalerErrorCode {
    */
   EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1356,
 
-  /**
-   * The number of coins to be created in refresh exceeds the limits of the 
exchange. private transfer keys request does not match #TALER_CNC_KAPPA - 1.
-   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
-   * (A value of 0 indicates that the error is generated client-side).
-   */
-  EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1357,
-
   /**
    * The number of envelopes given does not match the number of denomination 
keys given.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -582,6 +645,20 @@ export enum TalerErrorCode {
    */
   EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID = 1363,
 
+  /**
+   * The client provided age commitment data, but age restriction is not 
supported on this server.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED = 1364,
+
+  /**
+   * The client provided invalid age commitment data: missing, not an array, 
or  array of invalid size.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID = 1365,
+
   /**
    * The coin specified in the link request is unknown to the exchange.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -736,6 +813,34 @@ export enum TalerErrorCode {
    */
   EXCHANGE_RECOUP_NOT_ELIGIBLE = 1555,
 
+  /**
+   * The given coin signature is invalid for the request.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID = 1575,
+
+  /**
+   * The exchange could not find the corresponding melt operation. The request 
is denied.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND = 1576,
+
+  /**
+   * The exchange failed to reproduce the coin's blinding.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED = 1578,
+
+  /**
+   * The coin's denomination has not been revoked yet.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE = 1580,
+
   /**
    * This exchange does not allow clients to request /keys for times other 
than the current (exchange) time.
    * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -750,6 +855,27 @@ export enum TalerErrorCode {
    */
   EXCHANGE_WIRE_SIGNATURE_INVALID = 1650,
 
+  /**
+   * No bank accounts are enabled for the exchange. The administrator should 
enable-account using the taler-exchange-offline tool.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED = 1651,
+
+  /**
+   * The payto:// URI stored in the exchange database for its bank account is 
malformed.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED = 1652,
+
+  /**
+   * No wire fees are configured for an enabled wire method of the exchange. 
The administrator must set the wire-fee using the taler-exchange-offline tool.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_WIRE_FEES_NOT_CONFIGURED = 1653,
+
   /**
    * The exchange failed to talk to the process responsible for its private 
denomination keys.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
@@ -904,6 +1030,20 @@ export enum TalerErrorCode {
    */
   EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID = 1815,
 
+  /**
+   * The signature conflicts with a previous signature affirming different 
fees.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH = 1816,
+
+  /**
+   * The signature affirming the fee structure is invalid.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
+
   /**
    * The auditor signature over the denomination meta data is invalid.
    * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -925,6 +1065,41 @@ export enum TalerErrorCode {
    */
   EXCHANGE_AUDITORS_AUDITOR_INACTIVE = 1902,
 
+  /**
+   * The signature affirming the wallet's KYC request was invalid.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_KYC_WALLET_SIGNATURE_INVALID = 1925,
+
+  /**
+   * The exchange received an unexpected malformed response from its KYC 
backend.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE = 1926,
+
+  /**
+   * The backend signaled an unexpected failure.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_KYC_PROOF_BACKEND_ERROR = 1927,
+
+  /**
+   * The backend signaled an authorization failure.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED = 1928,
+
+  /**
+   * The payto-URI hash did not match. Hence the request was denied.
+   * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED = 1930,
+
   /**
    * The backend could not find the merchant instance specified in the request.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1044,6 +1219,13 @@ export enum TalerErrorCode {
    */
   MERCHANT_GENERIC_INSTANCE_DELETED = 2016,
 
+  /**
+   * The backend could not find the inbound wire transfer specified in the 
request.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_GENERIC_TRANSFER_UNKNOWN = 2017,
+
   /**
    * The exchange failed to provide a valid answer to the tracking request, 
thus those details are not in the response.
    * Returned with an HTTP status code of #MHD_HTTP_OK (200).
@@ -1066,12 +1248,19 @@ export enum TalerErrorCode {
   MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE = 2104,
 
   /**
-   * The token used to authenticate the client is invalid for this order.
+   * The claim token used to authenticate the client is invalid for this order.
    * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
    * (A value of 0 indicates that the error is generated client-side).
    */
   MERCHANT_GET_ORDERS_ID_INVALID_TOKEN = 2105,
 
+  /**
+   * The contract terms hash used to authenticate the client is invalid for 
this order.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH = 2106,
+
   /**
    * The exchange responded saying that funds were insufficient (for example, 
due to double-spending).
    * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1207,7 +1396,7 @@ export enum TalerErrorCode {
 
   /**
    * The contract hash does not match the given order ID.
-   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
   MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH = 2200,
@@ -1366,6 +1555,20 @@ export enum TalerErrorCode {
    */
   MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE = 2504,
 
+  /**
+   * The request is invalid: a delivery date was given, but it is in the past.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST = 2505,
+
+  /**
+   * The request is invalid: the wire deadline for the order would be "never".
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER = 2506,
+
   /**
    * One of the paths to forget is malformed.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1408,13 +1611,27 @@ export enum TalerErrorCode {
    */
   MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT = 2532,
 
+  /**
+   * The exchange says it does not know this transfer.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN = 2550,
+
   /**
    * We internally failed to execute the /track/transfer request.
-   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
    * (A value of 0 indicates that the error is generated client-side).
    */
   MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR = 2551,
 
+  /**
+   * The amount transferred differs between what was submitted and what the 
exchange claimed.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS = 2552,
+
   /**
    * The exchange gave conflicting information about a coin which has been 
wire transferred.
    * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1436,6 +1653,20 @@ export enum TalerErrorCode {
    */
   MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2555,
 
+  /**
+   * The backend could not delete the transfer as the echange already replied 
to our inquiry about it and we have integrated the result.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED = 2556,
+
+  /**
+   * The backend was previously informed about a wire transfer with the same 
ID but a different amount. Multiple wire transfers with the same ID are not 
allowed. If the new amount is correct, the old transfer should first be deleted.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION = 2557,
+
   /**
    * The merchant backend cannot create an instance under the given identifier 
as one already exists. Use PATCH to modify the existing entry.
    * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1487,7 +1718,7 @@ export enum TalerErrorCode {
 
   /**
    * The update would have mean that more stocks were lost than what remains 
from total inventory after sales, which is not allowed.
-   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
   MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS = 2661,
@@ -1499,6 +1730,13 @@ export enum TalerErrorCode {
    */
   MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED = 2662,
 
+  /**
+   * The update would have reduced the total amount of product sold, which is 
not allowed.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED = 2663,
+
   /**
    * The lock request is for more products than we have left (unlocked) in 
stock.
    * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1667,6 +1905,13 @@ export enum TalerErrorCode {
    */
   BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5113,
 
+  /**
+   * The wire transfer subject duplicates an existing reserve public key. But 
wire transfer subjects must be unique.
+   * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  BANK_DUPLICATE_RESERVE_PUB_SUBJECT = 5114,
+
   /**
    * The sync service failed find the account in its database.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1962,14 +2207,28 @@ export enum TalerErrorCode {
   ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED = 8007,
 
   /**
-   * The truth public key is unknown to the provider.
+   * The Anastasis provider could not be reached.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_GENERIC_PROVIDER_UNREACHABLE = 8008,
+
+  /**
+   * HTTP server experienced a timeout while awaiting promised payment.
+   * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_PAYMENT_GENERIC_TIMEOUT = 8009,
+
+  /**
+   * The key share is unknown to the provider.
    * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
    * (A value of 0 indicates that the error is generated client-side).
    */
   ANASTASIS_TRUTH_UNKNOWN = 8108,
 
   /**
-   * The authorization method used by the truth is no longer supported by the 
provider.
+   * The authorization method used for the key share is no longer supported by 
the provider.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -1990,8 +2249,8 @@ export enum TalerErrorCode {
   ANASTASIS_TRUTH_CHALLENGE_FAILED = 8111,
 
   /**
-   * The service is unaware of having issued a challenge.
-   * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+   * The backend is not aware of having issued the provided challenge code. 
Either this is the wrong code, or it has expired.
+   * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
    * (A value of 0 indicates that the error is generated client-side).
    */
   ANASTASIS_TRUTH_CHALLENGE_UNKNOWN = 8112,
@@ -2046,8 +2305,8 @@ export enum TalerErrorCode {
   ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR = 8119,
 
   /**
-   * The decryption of the truth object failed with the provided key.
-   * Returned with an HTTP status code of #MHD_HTTP_EXPECTATION_FAILED (417).
+   * The decryption of the key share failed with the provided key.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
   ANASTASIS_TRUTH_DECRYPTION_FAILED = 8120,
@@ -2060,14 +2319,28 @@ export enum TalerErrorCode {
   ANASTASIS_TRUTH_RATE_LIMITED = 8121,
 
   /**
-   * The backend failed to store the truth because the UUID is already in use.
+   * The authentication process did not yet complete. The user should try 
again later.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_TRUTH_AUTH_TIMEOUT = 8122,
+
+  /**
+   * A request to issue a challenge is not valid for this authentication 
method.
+   * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD = 8123,
+
+  /**
+   * The backend failed to store the key share because the UUID is already in 
use.
    * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
    * (A value of 0 indicates that the error is generated client-side).
    */
   ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS = 8150,
 
   /**
-   * The backend failed to store the truth because the authorization method is 
not supported.
+   * The backend failed to store the key share because the authorization 
method is not supported.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -2088,7 +2361,7 @@ export enum TalerErrorCode {
   ANASTASIS_SMS_HELPER_EXEC_FAILED = 8201,
 
   /**
-   * Helper terminated with a non-successful result.
+   * Provider failed to send SMS. Helper terminated with a non-successful 
result.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -2109,7 +2382,7 @@ export enum TalerErrorCode {
   ANASTASIS_EMAIL_HELPER_EXEC_FAILED = 8211,
 
   /**
-   * Helper terminated with a non-successful result.
+   * Provider failed to send E-mail. Helper terminated with a non-successful 
result.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -2130,12 +2403,33 @@ export enum TalerErrorCode {
   ANASTASIS_POST_HELPER_EXEC_FAILED = 8221,
 
   /**
-   * Helper terminated with a non-successful result.
+   * Provider failed to send mail. Helper terminated with a non-successful 
result.
    * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
    * (A value of 0 indicates that the error is generated client-side).
    */
   ANASTASIS_POST_HELPER_COMMAND_FAILED = 8222,
 
+  /**
+   * The provided IBAN address is not an acceptable IBAN.
+   * Returned with an HTTP status code of #MHD_HTTP_EXPECTATION_FAILED (417).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_IBAN_INVALID = 8230,
+
+  /**
+   * The backend did not find a TOTP key in the data provided.
+   * Returned with an HTTP status code of #MHD_HTTP_EXPECTATION_FAILED (417).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_TOTP_KEY_MISSING = 8240,
+
+  /**
+   * The key provided does not satisfy the format restrictions for an 
Anastasis TOTP key.
+   * Returned with an HTTP status code of #MHD_HTTP_EXPECTATION_FAILED (417).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_TOTP_KEY_INVALID = 8241,
+
   /**
    * The given if-none-match header is malformed.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2200,7 +2494,7 @@ export enum TalerErrorCode {
   ANASTASIS_REDUCER_INPUT_INVALID = 8402,
 
   /**
-   * The selected authentication method does ot work for the Anastasis 
provider.
+   * The selected authentication method does not work for the Anastasis 
provider.
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -2249,7 +2543,7 @@ export enum TalerErrorCode {
   ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED = 8409,
 
   /**
-   * Our attempts to download the recovery document failed with all providers.
+   * Our attempts to download the recovery document failed with all providers. 
Most likely the personal information you entered differs from the information 
you provided during the backup process and you should go back to the previous 
step. Alternatively, if you used a backup provider that is unknown to this 
application, you should add that provider manually.
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
    * (A value of 0 indicates that the error is generated client-side).
    */
@@ -2311,6 +2605,41 @@ export enum TalerErrorCode {
    */
   ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG = 8418,
 
+  /**
+   * The reducer encountered an internal error, likely a bug that needs to be 
reported.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  ANASTASIS_REDUCER_INTERNAL_ERROR = 8419,
+
+  /**
+   * A generic error happened in the LibEuFin nexus.  See the enclose details 
JSON for more information.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  LIBEUFIN_NEXUS_GENERIC_ERROR = 9000,
+
+  /**
+   * An uncaught exception happened in the LibEuFin nexus service.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION = 9001,
+
+  /**
+   * A generic error happened in the LibEuFin sandbox.  See the enclose 
details JSON for more information.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  LIBEUFIN_SANDBOX_GENERIC_ERROR = 9500,
+
+  /**
+   * An uncaught exception happened in the LibEuFin sandbox service.
+   * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR 
(500).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION = 9501,
+
   /**
    * End of error code range.
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
diff --git a/packages/taler-util/src/talerTypes.ts 
b/packages/taler-util/src/talerTypes.ts
index 4ea1b45f..b38f788a 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -410,11 +410,6 @@ export interface ContractTerms {
    */
   h_wire: string;
 
-  /**
-   * Legacy wire hash, used for deposit operations with an older exchange.
-   */
-  h_wire_legacy?: string;
-
   /**
    * Hash of the merchant's wire details.
    */
diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 2219316b..b8433e26 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -132,7 +132,7 @@ export interface ConfirmPayResultDone {
 export interface ConfirmPayResultPending {
   type: ConfirmPayResultType.Pending;
 
-  lastError: TalerErrorDetails;
+  lastError: TalerErrorDetails | undefined;
 }
 
 export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 52fc94b8..ac28d097 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -751,27 +751,27 @@ export enum ProposalStatus {
   /**
    * Not downloaded yet.
    */
-  DOWNLOADING = "downloading",
+  Downloading = "downloading",
   /**
    * Proposal downloaded, but the user needs to accept/reject it.
    */
-  PROPOSED = "proposed",
+  Proposed = "proposed",
   /**
    * The user has accepted the proposal.
    */
-  ACCEPTED = "accepted",
+  Accepted = "accepted",
   /**
    * The user has rejected the proposal.
    */
-  REFUSED = "refused",
+  Refused = "refused",
   /**
    * Downloading or processing the proposal has failed permanently.
    */
-  PERMANENTLY_FAILED = "permanently-failed",
+  PermanentlyFailed = "permanently-failed",
   /**
    * Downloaded proposal was detected as a re-purchase.
    */
-  REPURCHASE = "repurchase",
+  Repurchase = "repurchase",
 }
 
 export interface ProposalDownload {
@@ -831,7 +831,7 @@ export interface ProposalRecord {
   /**
    * Retry info, even present when the operation isn't active to allow indexing
    * on the next retry timestamp.
-   * 
+   *
    * FIXME: Clarify what we even retry.
    */
   retryInfo?: RetryInfo;
diff --git a/packages/taler-wallet-core/src/operations/README.md 
b/packages/taler-wallet-core/src/operations/README.md
index ca7140d6..426f2c55 100644
--- a/packages/taler-wallet-core/src/operations/README.md
+++ b/packages/taler-wallet-core/src/operations/README.md
@@ -2,6 +2,25 @@
 
 This folder contains the implementations for all wallet operations that 
operate on the wallet state.
 
-To avoid cyclic dependencies, these files must **not** reference each other.  
Instead, other operations should only be accessed via injected dependencies.
+To avoid cyclic dependencies, these files must **not** reference each other. 
Instead, other operations should only be accessed via injected dependencies.
 
 Avoiding cyclic dependencies is important for module bundlers.
+
+## Retries
+
+Many operations in the wallet are automatically retried when they fail or when 
the wallet
+is still waiting for some external condition (such as a wire transfer to the 
exchange).
+
+Retries are generally controlled by a "retryInfo" field in the corresponding 
database record. This field is set to undefined when no retry should be 
scheduled.
+
+Generally, the code to process a pending operation should first increment the
+retryInfo (and reset the lastError) and then process the operation. This way,
+it is impossble to forget incrementing the retryInfo.
+
+For each retriable operation, there are usually `reset<Op>Retry`, 
`increment<Op>Retry` and
+`report<Op>Error` operations.
+
+Note that this means that _during_ some operation, lastError will be cleared. 
The UI
+should accommodate for this.
+
+It would be possible to store a list of last errors, but we currently don't do 
that.
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts 
b/packages/taler-wallet-core/src/operations/backup/export.ts
index 05ef6688..12b30941 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -388,19 +388,19 @@ export async function exportBackup(
         }
         let propStatus: BackupProposalStatus;
         switch (prop.proposalStatus) {
-          case ProposalStatus.ACCEPTED:
+          case ProposalStatus.Accepted:
             return;
-          case ProposalStatus.DOWNLOADING:
-          case ProposalStatus.PROPOSED:
+          case ProposalStatus.Downloading:
+          case ProposalStatus.Proposed:
             propStatus = BackupProposalStatus.Proposed;
             break;
-          case ProposalStatus.PERMANENTLY_FAILED:
+          case ProposalStatus.PermanentlyFailed:
             propStatus = BackupProposalStatus.PermanentlyFailed;
             break;
-          case ProposalStatus.REFUSED:
+          case ProposalStatus.Refused:
             propStatus = BackupProposalStatus.Refused;
             break;
-          case ProposalStatus.REPURCHASE:
+          case ProposalStatus.Repurchase:
             propStatus = BackupProposalStatus.Repurchase;
             break;
         }
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 314d6efc..84acfb16 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -538,19 +538,19 @@ export async function importBackup(
           switch (backupProposal.proposal_status) {
             case BackupProposalStatus.Proposed:
               if (backupProposal.contract_terms_raw) {
-                proposalStatus = ProposalStatus.PROPOSED;
+                proposalStatus = ProposalStatus.Proposed;
               } else {
-                proposalStatus = ProposalStatus.DOWNLOADING;
+                proposalStatus = ProposalStatus.Downloading;
               }
               break;
             case BackupProposalStatus.Refused:
-              proposalStatus = ProposalStatus.REFUSED;
+              proposalStatus = ProposalStatus.Refused;
               break;
             case BackupProposalStatus.Repurchase:
-              proposalStatus = ProposalStatus.REPURCHASE;
+              proposalStatus = ProposalStatus.Repurchase;
               break;
             case BackupProposalStatus.PermanentlyFailed:
-              proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
+              proposalStatus = ProposalStatus.PermanentlyFailed;
               break;
           }
           if (backupProposal.contract_terms_raw) {
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 25b9cb92..e45da7b4 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -58,7 +58,6 @@ import {
   getCandidatePayCoins,
   getTotalPaymentCost,
   hashWire,
-  hashWireLegacy,
 } from "./pay.js";
 import { getTotalRefreshCost } from "./refresh.js";
 
@@ -443,7 +442,6 @@ export async function createDepositGroup(
   const merchantPair = await ws.cryptoApi.createEddsaKeypair();
   const wireSalt = encodeCrock(getRandomBytes(16));
   const wireHash = hashWire(req.depositPaytoUri, wireSalt);
-  const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt);
   const contractTerms: ContractTerms = {
     auditors: [],
     exchanges: exchangeInfos,
@@ -460,7 +458,6 @@ export async function createDepositGroup(
     // This is always the v2 wire hash, as we're the "merchant" and support v2.
     h_wire: wireHash,
     // Required for older exchanges.
-    h_wire_legacy: wireHashLegacy,
     pay_deadline: timestampAddDuration(
       timestampRound,
       durationFromSpec({ hours: 1 }),
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 4870d446..97d87e5c 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2019 Taler Systems S.A.
+ (C) 2019-2022 Taler Systems S.A.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
@@ -26,12 +26,40 @@
  */
 import {
   AmountJson,
-  Amounts, codecForContractTerms, codecForMerchantPayResponse, 
codecForProposal, CoinDepositPermission, ConfirmPayResult,
-  ConfirmPayResultType, ContractTerms, decodeCrock, DenomKeyType, Duration,
+  Amounts,
+  CheckPaymentResponse,
+  codecForContractTerms,
+  codecForMerchantPayResponse,
+  codecForProposal,
+  CoinDepositPermission,
+  ConfirmPayResult,
+  ConfirmPayResultType,
+  ContractTerms,
+  decodeCrock,
+  Duration,
   durationMax,
   durationMin,
-  durationMul, encodeCrock, getDurationRemaining, getRandomBytes, 
getTimestampNow, HttpStatusCode, isTimestampExpired, j2s, kdf, Logger, 
NotificationType, parsePayUri, PreparePayResult,
-  PreparePayResultType, RefreshReason, stringToBytes, TalerErrorCode, 
TalerErrorDetails, Timestamp, timestampAddDuration, URL
+  durationMul,
+  encodeCrock,
+  getDurationRemaining,
+  getRandomBytes,
+  getTimestampNow,
+  HttpStatusCode,
+  isTimestampExpired,
+  j2s,
+  kdf,
+  Logger,
+  NotificationType,
+  parsePayUri,
+  PreparePayResult,
+  PreparePayResultType,
+  RefreshReason,
+  stringToBytes,
+  TalerErrorCode,
+  TalerErrorDetails,
+  Timestamp,
+  timestampAddDuration,
+  URL,
 } from "@gnu-taler/taler-util";
 import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js";
 import {
@@ -46,16 +74,20 @@ import {
   ProposalStatus,
   PurchaseRecord,
   WalletContractData,
-  WalletStoresV1
+  WalletStoresV1,
 } from "../db.js";
 import {
   guardOperationException,
   makeErrorDetails,
   OperationFailedAndReportedError,
-  OperationFailedError
+  OperationFailedError,
 } from "../errors.js";
 import {
-  AvailableCoinInfo, CoinCandidateSelection, PayCoinSelection, 
PreviousPayCoins, selectPayCoins
+  AvailableCoinInfo,
+  CoinCandidateSelection,
+  PayCoinSelection,
+  PreviousPayCoins,
+  selectPayCoins,
 } from "../util/coinSelection.js";
 import { ContractTermsUtil } from "../util/contractTerms.js";
 import {
@@ -64,12 +96,13 @@ import {
   readSuccessResponseJsonOrThrow,
   readTalerErrorResponse,
   readUnexpectedResponseDetails,
-  throwUnexpectedRequestError
+  throwUnexpectedRequestError,
 } from "../util/http.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import {
-  getRetryDuration, initRetryInfo,
-  updateRetryInfoTimeout
+  getRetryDuration,
+  initRetryInfo,
+  updateRetryInfoTimeout,
 } from "../util/retries.js";
 import { getExchangeDetails } from "./exchanges.js";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
@@ -79,6 +112,9 @@ import { createRefreshGroup, getTotalRefreshCost } from 
"./refresh.js";
  */
 const logger = new Logger("pay.ts");
 
+/**
+ * FIXME: Move this to crypto worker or at least talerCrypto.ts
+ */
 export function hashWire(paytoUri: string, salt: string): string {
   const r = kdf(
     64,
@@ -89,16 +125,6 @@ export function hashWire(paytoUri: string, salt: string): 
string {
   return encodeCrock(r);
 }
 
-export function hashWireLegacy(paytoUri: string, salt: string): string {
-  const r = kdf(
-    64,
-    stringToBytes(paytoUri + "\0"),
-    stringToBytes(salt + "\0"),
-    stringToBytes("merchant-wire-signature"),
-  );
-  return encodeCrock(r);
-}
-
 /**
  * Compute the total cost of a payment to the customer.
  *
@@ -437,7 +463,7 @@ async function recordConfirmPay(
     .runReadWrite(async (tx) => {
       const p = await tx.proposals.get(proposal.proposalId);
       if (p) {
-        p.proposalStatus = ProposalStatus.ACCEPTED;
+        p.proposalStatus = ProposalStatus.Accepted;
         delete p.lastError;
         p.retryInfo = initRetryInfo();
         await tx.proposals.put(p);
@@ -453,10 +479,10 @@ async function recordConfirmPay(
   return t;
 }
 
-async function incrementProposalRetry(
+async function reportProposalError(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetails,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ proposals: x.proposals }))
@@ -466,24 +492,59 @@ async function incrementProposalRetry(
         return;
       }
       if (!pr.retryInfo) {
+        logger.error(
+          `Asked to report an error for a proposal (${proposalId}) that is not 
active (no retryInfo)`,
+        );
         return;
       }
-      pr.retryInfo.retryCounter++;
-      updateRetryInfoTimeout(pr.retryInfo);
       pr.lastError = err;
       await tx.proposals.put(pr);
     });
-  if (err) {
-    ws.notify({ type: NotificationType.ProposalOperationError, error: err });
-  }
+  ws.notify({ type: NotificationType.ProposalOperationError, error: err });
+}
+
+async function incrementProposalRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => ({ proposals: x.proposals }))
+    .runReadWrite(async (tx) => {
+      const pr = await tx.proposals.get(proposalId);
+      if (!pr) {
+        return;
+      }
+      if (!pr.retryInfo) {
+        return;
+      } else {
+        pr.retryInfo.retryCounter++;
+        updateRetryInfoTimeout(pr.retryInfo);
+      }
+      delete pr.lastError;
+      await tx.proposals.put(pr);
+    });
+}
+
+async function resetPurchasePayRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => ({ purchases: x.purchases }))
+    .runReadWrite(async (tx) => {
+      const p = await tx.purchases.get(proposalId);
+      if (p) {
+        p.payRetryInfo = initRetryInfo();
+        delete p.lastPayError;
+        await tx.purchases.put(p);
+      }
+    });
 }
 
 async function incrementPurchasePayRetry(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails | undefined,
 ): Promise<void> {
-  logger.warn("incrementing purchase pay retry with error", err);
   await ws.db
     .mktx((x) => ({ purchases: x.purchases }))
     .runReadWrite(async (tx) => {
@@ -496,16 +557,32 @@ async function incrementPurchasePayRetry(
       }
       pr.payRetryInfo.retryCounter++;
       updateRetryInfoTimeout(pr.payRetryInfo);
-      logger.trace(
-        `retrying pay in ${getDurationRemaining(pr.payRetryInfo.nextRetry).d_ms
-        } ms`,
-      );
+      delete pr.lastPayError;
+      await tx.purchases.put(pr);
+    });
+}
+
+async function reportPurchasePayError(
+  ws: InternalWalletState,
+  proposalId: string,
+  err: TalerErrorDetails,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => ({ purchases: x.purchases }))
+    .runReadWrite(async (tx) => {
+      const pr = await tx.purchases.get(proposalId);
+      if (!pr) {
+        return;
+      }
+      if (!pr.payRetryInfo) {
+        logger.error(
+          `purchase record (${proposalId}) reports error, but no retry active`,
+        );
+      }
       pr.lastPayError = err;
       await tx.purchases.put(pr);
     });
-  if (err) {
-    ws.notify({ type: NotificationType.PayOperationError, error: err });
-  }
+  ws.notify({ type: NotificationType.PayOperationError, error: err });
 }
 
 export async function processDownloadProposal(
@@ -514,7 +591,7 @@ export async function processDownloadProposal(
   forceNow = false,
 ): Promise<void> {
   const onOpErr = (err: TalerErrorDetails): Promise<void> =>
-    incrementProposalRetry(ws, proposalId, err);
+    reportProposalError(ws, proposalId, err);
   await guardOperationException(
     () => processDownloadProposalImpl(ws, proposalId, forceNow),
     onOpErr,
@@ -530,7 +607,8 @@ async function resetDownloadProposalRetry(
     .runReadWrite(async (tx) => {
       const p = await tx.proposals.get(proposalId);
       if (p) {
-        delete p.retryInfo;
+        p.retryInfo = initRetryInfo();
+        delete p.lastError;
         await tx.proposals.put(p);
       }
     });
@@ -550,7 +628,7 @@ async function failProposalPermanently(
       }
       delete p.retryInfo;
       p.lastError = err;
-      p.proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
+      p.proposalStatus = ProposalStatus.PermanentlyFailed;
       await tx.proposals.put(p);
     });
 }
@@ -618,21 +696,26 @@ async function processDownloadProposalImpl(
   proposalId: string,
   forceNow: boolean,
 ): Promise<void> {
-  if (forceNow) {
-    await resetDownloadProposalRetry(ws, proposalId);
-  }
   const proposal = await ws.db
     .mktx((x) => ({ proposals: x.proposals }))
     .runReadOnly(async (tx) => {
       return tx.proposals.get(proposalId);
     });
+
   if (!proposal) {
     return;
   }
-  if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
+
+  if (proposal.proposalStatus != ProposalStatus.Downloading) {
     return;
   }
 
+  if (forceNow) {
+    await resetDownloadProposalRetry(ws, proposalId);
+  } else {
+    await incrementProposalRetry(ws, proposalId);
+  }
+
   const orderClaimUrl = new URL(
     `orders/${proposal.orderId}/claim`,
     proposal.merchantBaseUrl,
@@ -771,7 +854,7 @@ async function processDownloadProposalImpl(
       if (!p) {
         return;
       }
-      if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
+      if (p.proposalStatus !== ProposalStatus.Downloading) {
         return;
       }
       p.download = {
@@ -787,13 +870,13 @@ async function processDownloadProposalImpl(
           await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
         if (differentPurchase) {
           logger.warn("repurchase detected");
-          p.proposalStatus = ProposalStatus.REPURCHASE;
+          p.proposalStatus = ProposalStatus.Repurchase;
           p.repurchaseProposalId = differentPurchase.proposalId;
           await tx.proposals.put(p);
           return;
         }
       }
-      p.proposalStatus = ProposalStatus.PROPOSED;
+      p.proposalStatus = ProposalStatus.Proposed;
       await tx.proposals.put(p);
     });
 
@@ -855,7 +938,7 @@ async function startDownloadProposal(
     merchantBaseUrl,
     orderId,
     proposalId: proposalId,
-    proposalStatus: ProposalStatus.DOWNLOADING,
+    proposalStatus: ProposalStatus.Downloading,
     repurchaseProposalId: undefined,
     retryInfo: initRetryInfo(),
     lastError: undefined,
@@ -975,10 +1058,14 @@ async function handleInsufficientFunds(
 
   const exchangeReply = (err as any).exchange_reply;
   if (
-    exchangeReply.code !== TalerErrorCode.EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS
+    exchangeReply.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS
   ) {
     // FIXME: set as failed
-    throw Error("can't handle error code");
+    if (logger.shouldLogTrace()) {
+      logger.trace("got exchange error reply (see below)");
+      logger.trace(j2s(exchangeReply));
+    }
+    throw Error(`unable to handle /pay error response 
(${exchangeReply.code})`);
   }
 
   logger.trace(`got error details: ${j2s(err)}`);
@@ -1083,213 +1170,6 @@ async function unblockBackup(
     });
 }
 
-/**
- * Submit a payment to the merchant.
- *
- * If the wallet has previously paid, it just transmits the merchant's
- * own signature certifying that the wallet has previously paid.
- */
-async function submitPay(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<ConfirmPayResult> {
-  const purchase = await ws.db
-    .mktx((x) => ({ purchases: x.purchases }))
-    .runReadOnly(async (tx) => {
-      return tx.purchases.get(proposalId);
-    });
-  if (!purchase) {
-    throw Error("Purchase not found: " + proposalId);
-  }
-  if (purchase.abortStatus !== AbortStatus.None) {
-    throw Error("not submitting payment for aborted purchase");
-  }
-  const sessionId = purchase.lastSessionId;
-
-  logger.trace("paying with session ID", sessionId);
-
-  //FIXME: not used, does it expect a side effect?
-  const merchantInfo = await ws.merchantOps.getMerchantInfo(
-    ws,
-    purchase.download.contractData.merchantBaseUrl,
-  );
-
-  if (!purchase.merchantPaySig) {
-    const payUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/pay`,
-      purchase.download.contractData.merchantBaseUrl,
-    ).href;
-
-    let depositPermissions: CoinDepositPermission[];
-
-    if (purchase.coinDepositPermissions) {
-      depositPermissions = purchase.coinDepositPermissions;
-    } else {
-      // FIXME: also cache!
-      depositPermissions = await generateDepositPermissions(
-        ws,
-        purchase.payCoinSelection,
-        purchase.download.contractData,
-      );
-    }
-
-    const reqBody = {
-      coins: depositPermissions,
-      session_id: purchase.lastSessionId,
-    };
-
-    logger.trace(
-      "making pay request ... ",
-      JSON.stringify(reqBody, undefined, 2),
-    );
-
-    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
-      ws.http.postJson(payUrl, reqBody, {
-        timeout: getPayRequestTimeout(purchase),
-      }),
-    );
-
-    logger.trace(`got resp ${JSON.stringify(resp)}`);
-
-    // Hide transient errors.
-    if (
-      (purchase.payRetryInfo?.retryCounter ?? 0) <= 5 &&
-      resp.status >= 500 &&
-      resp.status <= 599
-    ) {
-      logger.trace("treating /pay error as transient");
-      const err = makeErrorDetails(
-        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/pay failed",
-        getHttpResponseErrorDetails(resp),
-      );
-      incrementPurchasePayRetry(ws, proposalId, undefined);
-      return {
-        type: ConfirmPayResultType.Pending,
-        lastError: err,
-      };
-    }
-
-    if (resp.status === HttpStatusCode.BadRequest) {
-      const errDetails = await readUnexpectedResponseDetails(resp);
-      logger.warn("unexpected 400 response for /pay");
-      logger.warn(j2s(errDetails));
-      await ws.db
-        .mktx((x) => ({ purchases: x.purchases }))
-        .runReadWrite(async (tx) => {
-          const purch = await tx.purchases.get(proposalId);
-          if (!purch) {
-            return;
-          }
-          purch.payFrozen = true;
-          purch.lastPayError = errDetails;
-          delete purch.payRetryInfo;
-          await tx.purchases.put(purch);
-        });
-      // FIXME: Maybe introduce a new return type for this instead of throwing?
-      throw new OperationFailedAndReportedError(errDetails);
-    }
-
-    if (resp.status === HttpStatusCode.Conflict) {
-      const err = await readTalerErrorResponse(resp);
-      if (
-        err.code ===
-        TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
-      ) {
-        // Do this in the background, as it might take some time
-        handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
-          await incrementProposalRetry(ws, proposalId, {
-            code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-            message: "unexpected exception",
-            hint: "unexpected exception",
-            details: {
-              exception: e.toString(),
-            },
-          });
-        });
-
-        return {
-          type: ConfirmPayResultType.Pending,
-          // FIXME: should we return something better here?
-          lastError: err,
-        };
-      }
-    }
-
-    const merchantResp = await readSuccessResponseJsonOrThrow(
-      resp,
-      codecForMerchantPayResponse(),
-    );
-
-    logger.trace("got success from pay URL", merchantResp);
-
-    const merchantPub = purchase.download.contractData.merchantPub;
-    const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
-      merchantResp.sig,
-      purchase.download.contractData.contractTermsHash,
-      merchantPub,
-    );
-
-    if (!valid) {
-      logger.error("merchant payment signature invalid");
-      // FIXME: properly display error
-      throw Error("merchant payment signature invalid");
-    }
-
-    await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
-    await unblockBackup(ws, proposalId);
-  } else {
-    const payAgainUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/paid`,
-      purchase.download.contractData.merchantBaseUrl,
-    ).href;
-    const reqBody = {
-      sig: purchase.merchantPaySig,
-      h_contract: purchase.download.contractData.contractTermsHash,
-      session_id: sessionId ?? "",
-    };
-    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
-      ws.http.postJson(payAgainUrl, reqBody),
-    );
-    // Hide transient errors.
-    if (
-      (purchase.payRetryInfo?.retryCounter ?? 0) <= 5 &&
-      resp.status >= 500 &&
-      resp.status <= 599
-    ) {
-      const err = makeErrorDetails(
-        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/paid failed",
-        getHttpResponseErrorDetails(resp),
-      );
-      incrementPurchasePayRetry(ws, proposalId, undefined);
-      return {
-        type: ConfirmPayResultType.Pending,
-        lastError: err,
-      };
-    }
-    if (resp.status !== 204) {
-      throw OperationFailedError.fromCode(
-        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/paid failed",
-        getHttpResponseErrorDetails(resp),
-      );
-    }
-    await storePayReplaySuccess(ws, proposalId, sessionId);
-    await unblockBackup(ws, proposalId);
-  }
-
-  ws.notify({
-    type: NotificationType.PayOperationSuccess,
-    proposalId: purchase.proposalId,
-  });
-
-  return {
-    type: ConfirmPayResultType.Done,
-    contractTerms: purchase.download.contractTermsRaw,
-  };
-}
-
 export async function checkPaymentByProposalId(
   ws: InternalWalletState,
   proposalId: string,
@@ -1303,7 +1183,7 @@ export async function checkPaymentByProposalId(
   if (!proposal) {
     throw Error(`could not get proposal ${proposalId}`);
   }
-  if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
+  if (proposal.proposalStatus === ProposalStatus.Repurchase) {
     const existingProposalId = proposal.repurchaseProposalId;
     if (!existingProposalId) {
       throw Error("invalid proposal state");
@@ -1397,13 +1277,10 @@ export async function checkPaymentByProposalId(
           return;
         }
         p.lastSessionId = sessionId;
+        p.paymentSubmitPending = true;
         await tx.purchases.put(p);
       });
-    const r = await guardOperationException(
-      () => submitPay(ws, proposalId),
-      (e: TalerErrorDetails): Promise<void> =>
-        incrementPurchasePayRetry(ws, proposalId, e),
-    );
+    const r = await processPurchasePay(ws, proposalId, true);
     if (r.type !== ConfirmPayResultType.Done) {
       throw Error("submitting pay failed");
     }
@@ -1580,11 +1457,7 @@ export async function confirmPay(
 
   if (existingPurchase) {
     logger.trace("confirmPay: submitting payment for existing purchase");
-    return await guardOperationException(
-      () => submitPay(ws, proposalId),
-      (e: TalerErrorDetails): Promise<void> =>
-        incrementPurchasePayRetry(ws, proposalId, e),
-    );
+    return await processPurchasePay(ws, proposalId, true);
   }
 
   logger.trace("confirmPay: purchase record does not exist yet");
@@ -1634,62 +1507,233 @@ export async function confirmPay(
     sessionIdOverride,
   );
 
-  return await guardOperationException(
-    () => submitPay(ws, proposalId),
-    (e: TalerErrorDetails): Promise<void> =>
-      incrementPurchasePayRetry(ws, proposalId, e),
-  );
+  return await processPurchasePay(ws, proposalId, true);
 }
 
 export async function processPurchasePay(
   ws: InternalWalletState,
   proposalId: string,
   forceNow = false,
-): Promise<void> {
+): Promise<ConfirmPayResult> {
   const onOpErr = (e: TalerErrorDetails): Promise<void> =>
-    incrementPurchasePayRetry(ws, proposalId, e);
-  await guardOperationException(
+    reportPurchasePayError(ws, proposalId, e);
+  return await guardOperationException(
     () => processPurchasePayImpl(ws, proposalId, forceNow),
     onOpErr,
   );
 }
 
-async function resetPurchasePayRetry(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => ({ purchases: x.purchases }))
-    .runReadWrite(async (tx) => {
-      const p = await tx.purchases.get(proposalId);
-      if (p) {
-        p.payRetryInfo = initRetryInfo();
-        await tx.purchases.put(p);
-      }
-    });
-}
-
 async function processPurchasePayImpl(
   ws: InternalWalletState,
   proposalId: string,
   forceNow: boolean,
-): Promise<void> {
-  if (forceNow) {
-    await resetPurchasePayRetry(ws, proposalId);
-  }
+): Promise<ConfirmPayResult> {
   const purchase = await ws.db
     .mktx((x) => ({ purchases: x.purchases }))
     .runReadOnly(async (tx) => {
       return tx.purchases.get(proposalId);
     });
   if (!purchase) {
-    return;
+    return {
+      type: ConfirmPayResultType.Pending,
+      lastError: {
+        // FIXME: allocate more specific error code
+        code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+        message: `trying to pay for purchase that is not in the database`,
+        hint: `proposal ID is ${proposalId}`,
+        details: {},
+      },
+    };
   }
   if (!purchase.paymentSubmitPending) {
-    return;
+    return {
+      type: ConfirmPayResultType.Pending,
+      lastError: purchase.lastPayError,
+    };
+  }
+  if (forceNow) {
+    await resetPurchasePayRetry(ws, proposalId);
+  } else {
+    await incrementPurchasePayRetry(ws, proposalId);
   }
   logger.trace(`processing purchase pay ${proposalId}`);
-  await submitPay(ws, proposalId);
+
+  const sessionId = purchase.lastSessionId;
+
+  logger.trace("paying with session ID", sessionId);
+
+  if (!purchase.merchantPaySig) {
+    const payUrl = new URL(
+      `orders/${purchase.download.contractData.orderId}/pay`,
+      purchase.download.contractData.merchantBaseUrl,
+    ).href;
+
+    let depositPermissions: CoinDepositPermission[];
+
+    if (purchase.coinDepositPermissions) {
+      depositPermissions = purchase.coinDepositPermissions;
+    } else {
+      // FIXME: also cache!
+      depositPermissions = await generateDepositPermissions(
+        ws,
+        purchase.payCoinSelection,
+        purchase.download.contractData,
+      );
+    }
+
+    const reqBody = {
+      coins: depositPermissions,
+      session_id: purchase.lastSessionId,
+    };
+
+    logger.trace(
+      "making pay request ... ",
+      JSON.stringify(reqBody, undefined, 2),
+    );
+
+    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+      ws.http.postJson(payUrl, reqBody, {
+        timeout: getPayRequestTimeout(purchase),
+      }),
+    );
+
+    logger.trace(`got resp ${JSON.stringify(resp)}`);
+
+    // Hide transient errors.
+    if (
+      (purchase.payRetryInfo?.retryCounter ?? 0) <= 5 &&
+      resp.status >= 500 &&
+      resp.status <= 599
+    ) {
+      logger.trace("treating /pay error as transient");
+      const err = makeErrorDetails(
+        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+        "/pay failed",
+        getHttpResponseErrorDetails(resp),
+      );
+      return {
+        type: ConfirmPayResultType.Pending,
+        lastError: err,
+      };
+    }
+
+    if (resp.status === HttpStatusCode.BadRequest) {
+      const errDetails = await readUnexpectedResponseDetails(resp);
+      logger.warn("unexpected 400 response for /pay");
+      logger.warn(j2s(errDetails));
+      await ws.db
+        .mktx((x) => ({ purchases: x.purchases }))
+        .runReadWrite(async (tx) => {
+          const purch = await tx.purchases.get(proposalId);
+          if (!purch) {
+            return;
+          }
+          purch.payFrozen = true;
+          purch.lastPayError = errDetails;
+          delete purch.payRetryInfo;
+          await tx.purchases.put(purch);
+        });
+      // FIXME: Maybe introduce a new return type for this instead of throwing?
+      throw new OperationFailedAndReportedError(errDetails);
+    }
+
+    if (resp.status === HttpStatusCode.Conflict) {
+      const err = await readTalerErrorResponse(resp);
+      if (
+        err.code ===
+        TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS
+      ) {
+        // Do this in the background, as it might take some time
+        handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
+          reportPurchasePayError(ws, proposalId, {
+            code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+            message: "unexpected exception",
+            hint: "unexpected exception",
+            details: {
+              exception: e.toString(),
+            },
+          });
+        });
+
+        return {
+          type: ConfirmPayResultType.Pending,
+          // FIXME: should we return something better here?
+          lastError: err,
+        };
+      }
+    }
+
+    const merchantResp = await readSuccessResponseJsonOrThrow(
+      resp,
+      codecForMerchantPayResponse(),
+    );
+
+    logger.trace("got success from pay URL", merchantResp);
+
+    const merchantPub = purchase.download.contractData.merchantPub;
+    const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
+      merchantResp.sig,
+      purchase.download.contractData.contractTermsHash,
+      merchantPub,
+    );
+
+    if (!valid) {
+      logger.error("merchant payment signature invalid");
+      // FIXME: properly display error
+      throw Error("merchant payment signature invalid");
+    }
+
+    await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
+    await unblockBackup(ws, proposalId);
+  } else {
+    const payAgainUrl = new URL(
+      `orders/${purchase.download.contractData.orderId}/paid`,
+      purchase.download.contractData.merchantBaseUrl,
+    ).href;
+    const reqBody = {
+      sig: purchase.merchantPaySig,
+      h_contract: purchase.download.contractData.contractTermsHash,
+      session_id: sessionId ?? "",
+    };
+    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+      ws.http.postJson(payAgainUrl, reqBody),
+    );
+    // Hide transient errors.
+    if (
+      (purchase.payRetryInfo?.retryCounter ?? 0) <= 5 &&
+      resp.status >= 500 &&
+      resp.status <= 599
+    ) {
+      const err = makeErrorDetails(
+        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+        "/paid failed",
+        getHttpResponseErrorDetails(resp),
+      );
+      return {
+        type: ConfirmPayResultType.Pending,
+        lastError: err,
+      };
+    }
+    if (resp.status !== 204) {
+      throw OperationFailedError.fromCode(
+        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+        "/paid failed",
+        getHttpResponseErrorDetails(resp),
+      );
+    }
+    await storePayReplaySuccess(ws, proposalId, sessionId);
+    await unblockBackup(ws, proposalId);
+  }
+
+  ws.notify({
+    type: NotificationType.PayOperationSuccess,
+    proposalId: purchase.proposalId,
+  });
+
+  return {
+    type: ConfirmPayResultType.Done,
+    contractTerms: purchase.download.contractTermsRaw,
+  };
 }
 
 export async function refuseProposal(
@@ -1704,10 +1748,10 @@ export async function refuseProposal(
         logger.trace(`proposal ${proposalId} not found, won't refuse 
proposal`);
         return false;
       }
-      if (proposal.proposalStatus !== ProposalStatus.PROPOSED) {
+      if (proposal.proposalStatus !== ProposalStatus.Proposed) {
         return false;
       }
-      proposal.proposalStatus = ProposalStatus.REFUSED;
+      proposal.proposalStatus = ProposalStatus.Refused;
       await tx.proposals.put(proposal);
       return true;
     });
diff --git a/packages/taler-wallet-core/src/operations/pending.ts 
b/packages/taler-wallet-core/src/operations/pending.ts
index f615e8e5..6d686fb3 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -173,9 +173,9 @@ async function gatherProposalPending(
   resp: PendingOperationsResponse,
 ): Promise<void> {
   await tx.proposals.iter().forEach((proposal) => {
-    if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
+    if (proposal.proposalStatus == ProposalStatus.Proposed) {
       // Nothing to do, user needs to choose.
-    } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
+    } else if (proposal.proposalStatus == ProposalStatus.Downloading) {
       const timestampDue = proposal.retryInfo?.nextRetry ?? getTimestampNow();
       resp.pendingOperations.push({
         type: PendingTaskType.ProposalDownload,
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index a5846f25..106c7936 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -65,6 +65,23 @@ import { InternalWalletState } from "../common.js";
 
 const logger = new Logger("refund.ts");
 
+async function resetPurchaseQueryRefundRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => ({
+      purchases: x.purchases,
+    }))
+    .runReadWrite(async (tx) => {
+      const x = await tx.purchases.get(proposalId);
+      if (x) {
+        x.refundStatusRetryInfo = initRetryInfo();
+        await tx.purchases.put(x);
+      }
+    });
+}
+
 /**
  * Retry querying and applying refunds for an order later.
  */
@@ -578,23 +595,6 @@ export async function processPurchaseQueryRefund(
   );
 }
 
-async function resetPurchaseQueryRefundRetry(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => ({
-      purchases: x.purchases,
-    }))
-    .runReadWrite(async (tx) => {
-      const x = await tx.purchases.get(proposalId);
-      if (x) {
-        x.refundStatusRetryInfo = initRetryInfo();
-        await tx.purchases.put(x);
-      }
-    });
-}
-
 async function processPurchaseQueryRefundImpl(
   ws: InternalWalletState,
   proposalId: string,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]