aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-03-08 23:09:20 +0100
committerFlorian Dold <florian@dold.me>2022-03-08 23:09:20 +0100
commit6ee0354940c09d1065c3b3b7bf08e41fd6014268 (patch)
tree7d127977b1517702082d0253099f25c3fa26289f
parentd5a933e4cb685aab3e5e6bae5ca2358291e59130 (diff)
wallet: improve retry handling for payments, update error codes
-rw-r--r--packages/taler-util/src/taler-error-codes.ts431
-rw-r--r--packages/taler-util/src/talerTypes.ts5
-rw-r--r--packages/taler-util/src/walletTypes.ts2
-rw-r--r--packages/taler-wallet-core/src/db.ts14
-rw-r--r--packages/taler-wallet-core/src/operations/README.md21
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts12
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts646
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/refund.ts34
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 fec2cf0ff..b22f29a19 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -37,6 +37,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -93,6 +100,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -219,6 +233,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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,
@@ -296,6 +317,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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.
@@ -429,6 +506,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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.
@@ -485,13 +562,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -534,13 +604,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -583,6 +646,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -737,6 +814,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -751,6 +856,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -905,6 +1031,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -926,6 +1066,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1045,6 +1220,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1066,13 +1248,20 @@ 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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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,
@@ -1367,6 +1556,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1409,13 +1612,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1437,6 +1654,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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,
@@ -1500,6 +1731,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -1668,6 +1906,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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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,13 +2403,34 @@ 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).
* (A value of 0 indicates that the error is generated client-side).
@@ -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).
*/
@@ -2312,6 +2606,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).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index 4ea1b45fc..b38f788af 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -411,11 +411,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.
*/
auto_refund?: Duration;
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 2219316b3..b8433e261 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 52fc94b8d..ac28d0979 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 ca7140d6a..426f2c553 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 05ef66883..12b309418 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 314d6efc7..84acfb16c 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 25b9cb92d..e45da7b4c 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 4870d446a..97d87e5cc 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 f615e8e5d..6d686fb3a 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 a5846f259..106c79365 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,