gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: wallet: improve error handling and error code


From: gnunet
Subject: [taler-wallet-core] 02/02: wallet: improve error handling and error codes
Date: Tue, 22 Mar 2022 21:16:48 +0100

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

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

commit 5d23eb36354d07508a015531f298b3e261bbafce
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Mar 22 21:16:38 2022 +0100

    wallet: improve error handling and error codes
---
 packages/taler-util/src/notifications.ts           |  26 +--
 packages/taler-util/src/taler-error-codes.ts       |  70 ++++++-
 packages/taler-util/src/transactionsTypes.ts       |   4 +-
 packages/taler-util/src/walletTypes.ts             |  14 +-
 packages/taler-wallet-cli/src/harness/harness.ts   |  14 +-
 packages/taler-wallet-cli/src/index.ts             |  21 +-
 .../src/integrationtests/test-bank-api.ts          |   5 +-
 .../src/integrationtests/test-denom-unoffered.ts   |  32 +--
 .../integrationtests/test-exchange-management.ts   |  18 +-
 .../src/integrationtests/test-pay-abort.ts         |   2 +-
 .../src/integrationtests/test-payment-claim.ts     |  30 +--
 .../src/integrationtests/test-payment-transient.ts |   8 +-
 .../src/integrationtests/test-wallet-dbless.ts     |   6 +-
 .../integrationtests/test-withdrawal-abort-bank.ts |   4 +-
 packages/taler-wallet-core/src/bank-api-client.ts  |  19 +-
 packages/taler-wallet-core/src/db.ts               |  28 +--
 packages/taler-wallet-core/src/errors.ts           | 216 +++++++++++++--------
 .../taler-wallet-core/src/headless/NodeHttpLib.ts  |  46 ++---
 .../src/operations/backup/index.ts                 |  10 +-
 .../taler-wallet-core/src/operations/deposits.ts   |   6 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |  22 +--
 packages/taler-wallet-core/src/operations/pay.ts   | 106 +++++-----
 .../taler-wallet-core/src/operations/recoup.ts     |   6 +-
 .../taler-wallet-core/src/operations/refresh.ts    |   6 +-
 .../taler-wallet-core/src/operations/refund.ts     |   6 +-
 .../taler-wallet-core/src/operations/reserves.ts   |  13 +-
 packages/taler-wallet-core/src/operations/tip.ts   |  16 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |  35 ++--
 packages/taler-wallet-core/src/pending-types.ts    |  20 +-
 packages/taler-wallet-core/src/util/http.ts        |  92 ++++-----
 packages/taler-wallet-core/src/wallet.ts           |  62 +++---
 packages/taler-wallet-embedded/src/index.ts        |   8 +-
 .../src/browserHttpLib.ts                          |  22 +--
 .../src/components/ErrorTalerOperation.tsx         |   6 +-
 .../taler-wallet-webextension/src/cta/Deposit.tsx  |  14 +-
 packages/taler-wallet-webextension/src/cta/Pay.tsx |   4 +-
 .../taler-wallet-webextension/src/cta/Withdraw.tsx |  13 +-
 .../src/hooks/useAsyncAsHook.ts                    |  30 ++-
 .../src/serviceWorkerHttpLib.ts                    |  18 +-
 packages/taler-wallet-webextension/src/wxApi.ts    |  73 +++++--
 .../taler-wallet-webextension/src/wxBackend.ts     |  11 +-
 41 files changed, 663 insertions(+), 499 deletions(-)

diff --git a/packages/taler-util/src/notifications.ts 
b/packages/taler-util/src/notifications.ts
index e8f27062..b3d9ad1d 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -22,7 +22,7 @@
 /**
  * Imports.
  */
-import { TalerErrorDetails } from "./walletTypes.js";
+import { TalerErrorDetail } from "./walletTypes.js";
 
 export enum NotificationType {
   CoinWithdrawn = "coin-withdrawn",
@@ -157,62 +157,62 @@ export interface ExchangeAddedNotification {
 
 export interface ExchangeOperationErrorNotification {
   type: NotificationType.ExchangeOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface RefreshOperationErrorNotification {
   type: NotificationType.RefreshOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface BackupOperationErrorNotification {
   type: NotificationType.BackupOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface RefundStatusOperationErrorNotification {
   type: NotificationType.RefundStatusOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface RefundApplyOperationErrorNotification {
   type: NotificationType.RefundApplyOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface PayOperationErrorNotification {
   type: NotificationType.PayOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface ProposalOperationErrorNotification {
   type: NotificationType.ProposalOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface TipOperationErrorNotification {
   type: NotificationType.TipOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface WithdrawOperationErrorNotification {
   type: NotificationType.WithdrawOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface RecoupOperationErrorNotification {
   type: NotificationType.RecoupOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface DepositOperationErrorNotification {
   type: NotificationType.DepositOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface ReserveOperationErrorNotification {
   type: NotificationType.ReserveOperationError;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface ReserveCreatedNotification {
diff --git a/packages/taler-util/src/taler-error-codes.ts 
b/packages/taler-util/src/taler-error-codes.ts
index b22f29a1..8ea97f7e 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -22,6 +22,8 @@
  */
 
 export enum TalerErrorCode {
+
+
   /**
    * Special code to indicate success (no error).
    * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -78,6 +80,13 @@ export enum TalerErrorCode {
    */
   GENERIC_CONFIGURATION_INVALID = 14,
 
+  /**
+   * The client made a request to a service, but received an error response it 
does not know how to handle.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  GENERIC_UNEXPECTED_REQUEST_ERROR = 15,
+
   /**
    * The HTTP method used is invalid for this endpoint.
    * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
@@ -372,6 +381,20 @@ export enum TalerErrorCode {
    */
   EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
 
+  /**
+   * The reserve public key was 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).
+   */
+  EXCHANGE_GENERIC_RESERVE_PUB_MALFORMED = 1019,
+
+  /**
+   * The time at the server is too far off from the time specified in the 
request. Most likely the client system time is wrong.
+   * 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_CLOCK_SKEW = 1020,
+
   /**
    * 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).
@@ -541,11 +564,25 @@ export enum TalerErrorCode {
   EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
 
   /**
-   * The reserve status was requested using a unknown key.
+   * The reserve balance, status or history was requested for a reserve which 
is not known 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).
    */
-  EXCHANGE_RESERVES_GET_STATUS_UNKNOWN = 1250,
+  EXCHANGE_RESERVES_STATUS_UNKNOWN = 1250,
+
+  /**
+   * The reserve status was requested with a bad signature.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE = 1251,
+
+  /**
+   * The reserve history was requested with a bad signature.
+   * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE = 1252,
 
   /**
    * The exchange encountered melt fees exceeding the melted coin's 
contribution.
@@ -1394,6 +1431,27 @@ export enum TalerErrorCode {
    */
   MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
 
+  /**
+   * The payment required a minimum age but one of the coins (of a 
denomination with support for age restriction) did not provide any 
age_commitment.
+   * 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_PAY_AGE_COMMITMENT_MISSING = 2171,
+
+  /**
+   * The payment required a minimum age but one of the coins provided an 
age_commitment that contained a wrong number of public keys compared to the 
number of age groups defined in the denomination of the coin.
+   * 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_PAY_AGE_COMMITMENT_SIZE_MISMATCH = 2172,
+
+  /**
+   * The payment required a minimum age but one of the coins provided a 
minimum_age_sig that couldn't be verified with the given age_commitment for 
that particular minimum age.
+   * 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_PAY_AGE_VERIFICATION_FAILED = 2173,
+
   /**
    * The contract hash does not match the given order ID.
    * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2150,6 +2208,13 @@ export enum TalerErrorCode {
    */
   WALLET_CONTRACT_TERMS_MALFORMED = 7020,
 
+  /**
+   * A pending operation failed, and thus the request can't be completed.
+   * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+   * (A value of 0 indicates that the error is generated client-side).
+   */
+  WALLET_PENDING_OPERATION_FAILED = 7021,
+
   /**
    * We encountered a timeout with our payment backend.
    * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -2646,4 +2711,5 @@ export enum TalerErrorCode {
    * (A value of 0 indicates that the error is generated client-side).
    */
   END = 9999,
+
 }
diff --git a/packages/taler-util/src/transactionsTypes.ts 
b/packages/taler-util/src/transactionsTypes.ts
index bccbc773..b9a227b6 100644
--- a/packages/taler-util/src/transactionsTypes.ts
+++ b/packages/taler-util/src/transactionsTypes.ts
@@ -42,7 +42,7 @@ import {
   codecForList,
   codecForAny,
 } from "./codec.js";
-import { TalerErrorDetails } from "./walletTypes.js";
+import { TalerErrorDetail } from "./walletTypes.js";
 
 export interface TransactionsRequest {
   /**
@@ -92,7 +92,7 @@ export interface TransactionCommon {
   // Amount added or removed from the wallet's balance (including all fees and 
other costs)
   amountEffective: AmountString;
 
-  error?: TalerErrorDetails;
+  error?: TalerErrorDetail;
 }
 
 export type Transaction =
diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 3c4fa96c..1f88c39e 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -60,6 +60,7 @@ import {
 import { OrderShortInfo, codecForOrderShortInfo } from 
"./transactionsTypes.js";
 import { BackupRecovery } from "./backupTypes.js";
 import { PaytoUri } from "./payto.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
 
 /**
  * Response for the create reserve request to the wallet.
@@ -136,7 +137,7 @@ export interface ConfirmPayResultDone {
 export interface ConfirmPayResultPending {
   type: ConfirmPayResultType.Pending;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
@@ -455,11 +456,10 @@ export interface WalletDiagnostics {
   dbOutdated: boolean;
 }
 
-export interface TalerErrorDetails {
-  code: number;
-  hint: string;
-  message: string;
-  details: unknown;
+export interface TalerErrorDetail {
+  code: TalerErrorCode;
+  hint?: string;
+  [x: string]: unknown;
 }
 
 /**
@@ -850,7 +850,7 @@ export interface CoreApiResponseError {
   type: "error";
   operation: string;
   id: string;
-  error: TalerErrorDetails;
+  error: TalerErrorDetail;
 }
 
 export interface WithdrawTestBalanceRequest {
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts 
b/packages/taler-wallet-cli/src/harness/harness.ts
index 63bb17fc..46ddd0ed 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -49,7 +49,7 @@ import {
   HarnessExchangeBankAccount,
   NodeHttpLib,
   openPromise,
-  OperationFailedError,
+  TalerError,
   WalletCoreApiClient,
 } from "@gnu-taler/taler-wallet-core";
 import {
@@ -227,19 +227,19 @@ export class GlobalTestState {
     this.servers = [];
   }
 
-  async assertThrowsOperationErrorAsync(
+  async assertThrowsTalerErrorAsync(
     block: () => Promise<void>,
-  ): Promise<OperationFailedError> {
+  ): Promise<TalerError> {
     try {
       await block();
     } catch (e) {
-      if (e instanceof OperationFailedError) {
+      if (e instanceof TalerError) {
         return e;
       }
-      throw Error(`expected OperationFailedError to be thrown, but got ${e}`);
+      throw Error(`expected TalerError to be thrown, but got ${e}`);
     }
     throw Error(
-      `expected OperationFailedError to be thrown, but block finished without 
throwing`,
+      `expected TalerError to be thrown, but block finished without throwing`,
     );
   }
 
@@ -1904,7 +1904,7 @@ export class WalletCli {
           throw new Error("wallet CLI did not return a proper JSON response");
         }
         if (ar.type === "error") {
-          throw new OperationFailedError(ar.error);
+          throw TalerError.fromUncheckedDetail(ar.error);
         }
         return ar.result;
       },
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index f754ca91..e7b76fa9 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -49,14 +49,13 @@ import {
 import {
   NodeHttpLib,
   getDefaultNodeWallet,
-  OperationFailedAndReportedError,
-  OperationFailedError,
   NodeThreadCryptoWorkerFactory,
   CryptoApi,
   walletCoreDebugFlags,
   WalletApiOperation,
   WalletCoreApiClient,
   Wallet,
+  getErrorDetailFromException,
 } from "@gnu-taler/taler-wallet-core";
 import { lintExchangeDeployment } from "./lint.js";
 import { runBench1 } from "./bench1.js";
@@ -206,18 +205,12 @@ async function withWallet<T>(
     const ret = await f(w);
     return ret;
   } catch (e) {
-    if (
-      e instanceof OperationFailedAndReportedError ||
-      e instanceof OperationFailedError
-    ) {
-      console.error("Operation failed: " + e.message);
-      console.error(
-        "Error details:",
-        JSON.stringify(e.operationError, undefined, 2),
-      );
-    } else {
-      console.error("caught unhandled exception (bug?):", e);
-    }
+    const ed = getErrorDetailFromException(e);
+    console.error("Operation failed: " + ed.message);
+    console.error(
+      "Error details:",
+      JSON.stringify(ed.operationError, undefined, 2),
+    );
     process.exit(1);
   } finally {
     logger.info("operation with wallet finished, stopping");
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
index 8a11b79c..97dbf369 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts
@@ -32,7 +32,6 @@ import {
   BankApi,
   BankAccessApi,
   CreditDebitIndicator,
-  OperationFailedError,
 } from "@gnu-taler/taler-wallet-core";
 
 /**
@@ -104,10 +103,10 @@ export async function runBankApiTest(t: GlobalTestState) {
 
   // Make sure that registering twice results in a 409 Conflict
   {
-    const e = await t.assertThrowsAsync(async () => {
+    const e = await t.assertThrowsTalerErrorAsync(async () => {
       await BankApi.registerAccount(bank, "user1", "pw1");
     });
-    t.assertTrue(e.details.httpStatusCode === 409);
+    t.assertTrue(e.errorDetail.httpStatusCode === 409);
   }
 
   let balResp = await BankAccessApi.getAccountBalance(bank, bankUser);
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
index 28cca075..ec1d9f64 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
@@ -20,25 +20,21 @@
 import {
   PreparePayResultType,
   TalerErrorCode,
-  TalerErrorDetails,
-  TransactionType,
+  TalerErrorDetail,
 } from "@gnu-taler/taler-util";
-import {
-  WalletApiOperation,
-} from "@gnu-taler/taler-wallet-core";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 import { makeEventId } from "@gnu-taler/taler-wallet-core";
 import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from 
"../harness/helpers.js";
+import {
+  createSimpleTestkudosEnvironment,
+  withdrawViaBank,
+} from "../harness/helpers.js";
 
 export async function runDenomUnofferedTest(t: GlobalTestState) {
   // Set up test environment
 
-  const {
-    wallet,
-    bank,
-    exchange,
-    merchant,
-  } = await createSimpleTestkudosEnvironment(t);
+  const { wallet, bank, exchange, merchant } =
+    await createSimpleTestkudosEnvironment(t);
 
   // Withdraw digital cash into the wallet.
 
@@ -95,19 +91,23 @@ export async function runDenomUnofferedTest(t: 
GlobalTestState) {
       preparePayResult.status === PreparePayResultType.PaymentPossible,
     );
 
-    const exc = await t.assertThrowsAsync(async () => {
+    const exc = await t.assertThrowsTalerErrorAsync(async () => {
       await wallet.client.call(WalletApiOperation.ConfirmPay, {
         proposalId: preparePayResult.proposalId,
       });
     });
 
-    const errorDetails: TalerErrorDetails = exc.operationError;
+    t.assertTrue(
+      exc.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
+    );
+
     // FIXME: We might want a more specific error code here!
     t.assertDeepEqual(
-      errorDetails.code,
+      exc.errorDetail.innerError.code,
       TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
     );
-    const merchantErrorCode = (errorDetails.details as any).errorResponse.code;
+    const merchantErrorCode = (exc.errorDetail.innerError.errorResponse as any)
+      .code;
     t.assertDeepEqual(
       merchantErrorCode,
       TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
index f9c7c4b9..dc650830 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts
@@ -181,16 +181,22 @@ export async function runExchangeManagementTest(t: 
GlobalTestState) {
     },
   });
 
-  const err1 = await t.assertThrowsOperationErrorAsync(async () => {
+  const err1 = await t.assertThrowsTalerErrorAsync(async () => {
     await wallet.client.call(WalletApiOperation.AddExchange, {
       exchangeBaseUrl: faultyExchange.baseUrl,
     });
   });
 
+  // Updating the exchange from the base URL is technically a pending operation
+  // and it will be retried later.
+  t.assertTrue(
+    err1.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
+  );
+
   // Response is malformed, since it didn't even contain a version code
   // in a format the wallet can understand.
   t.assertTrue(
-    err1.operationError.code ===
+    err1.errorDetail.innerError.code ===
       TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
   );
 
@@ -223,14 +229,18 @@ export async function runExchangeManagementTest(t: 
GlobalTestState) {
     },
   });
 
-  const err2 = await t.assertThrowsOperationErrorAsync(async () => {
+  const err2 = await t.assertThrowsTalerErrorAsync(async () => {
     await wallet.client.call(WalletApiOperation.AddExchange, {
       exchangeBaseUrl: faultyExchange.baseUrl,
     });
   });
 
   t.assertTrue(
-    err2.operationError.code ===
+    err2.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
+  );
+
+  t.assertTrue(
+    err2.errorDetail.innerError.code ===
       TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
   );
 
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts
index 0fa9ec81..09b546f4 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts
@@ -123,7 +123,7 @@ export async function runPayAbortTest(t: GlobalTestState) {
     },
   });
 
-  await t.assertThrowsOperationErrorAsync(async () => {
+  await t.assertThrowsTalerErrorAsync(async () => {
     await wallet.client.call(WalletApiOperation.ConfirmPay, {
       proposalId: preparePayResult.proposalId,
     });
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts
index ba3bd8e0..e878854f 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts
@@ -17,8 +17,15 @@
 /**
  * Imports.
  */
-import { GlobalTestState, MerchantPrivateApi, WalletCli } from 
"../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from 
"../harness/helpers.js";
+import {
+  GlobalTestState,
+  MerchantPrivateApi,
+  WalletCli,
+} from "../harness/harness.js";
+import {
+  createSimpleTestkudosEnvironment,
+  withdrawViaBank,
+} from "../harness/helpers.js";
 import { PreparePayResultType } from "@gnu-taler/taler-util";
 import { TalerErrorCode } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -29,12 +36,8 @@ import { WalletApiOperation } from 
"@gnu-taler/taler-wallet-core";
 export async function runPaymentClaimTest(t: GlobalTestState) {
   // Set up test environment
 
-  const {
-    wallet,
-    bank,
-    exchange,
-    merchant,
-  } = await createSimpleTestkudosEnvironment(t);
+  const { wallet, bank, exchange, merchant } =
+    await createSimpleTestkudosEnvironment(t);
 
   const walletTwo = new WalletCli(t, "two");
 
@@ -73,7 +76,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) 
{
     preparePayResult.status === PreparePayResultType.PaymentPossible,
   );
 
-  t.assertThrowsOperationErrorAsync(async () => {
+  t.assertThrowsTalerErrorAsync(async () => {
     await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
       talerPayUri,
     });
@@ -93,14 +96,19 @@ export async function runPaymentClaimTest(t: 
GlobalTestState) {
 
   walletTwo.deleteDatabase();
 
-  const err = await t.assertThrowsOperationErrorAsync(async () => {
+  const err = await t.assertThrowsTalerErrorAsync(async () => {
     await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
       talerPayUri,
     });
   });
 
   t.assertTrue(
-    err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
+    err.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
+  );
+
+  t.assertTrue(
+    err.errorDetail.innerError.code ===
+      TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
   );
 
   await t.shutdown();
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts
index 75d44d49..7e178077 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts
@@ -32,7 +32,7 @@ import {
   ConfirmPayResultType,
   PreparePayResultType,
   TalerErrorCode,
-  TalerErrorDetails,
+  TalerErrorDetail,
   URL,
 } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -135,11 +135,9 @@ export async function runPaymentTransientTest(t: 
GlobalTestState) {
       }
       faultInjected = true;
       console.log("injecting pay fault");
-      const err: TalerErrorDetails = {
+      const err: TalerErrorDetail = {
         code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED,
-        details: {},
-        hint: "huh",
-        message: "something went wrong",
+        hint: "something went wrong",
       };
       ctx.responseBody = Buffer.from(JSON.stringify(err));
       ctx.statusCode = 500;
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts
index 93c22af7..146603f3 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts
@@ -26,9 +26,9 @@ import {
   findDenomOrThrow,
   generateReserveKeypair,
   NodeHttpLib,
-  OperationFailedError,
   refreshCoin,
   SynchronousCryptoWorkerFactory,
+  TalerError,
   topupReserveWithDemobank,
   withdrawCoin,
 } from "@gnu-taler/taler-wallet-core";
@@ -95,9 +95,9 @@ export async function runWalletDblessTest(t: GlobalTestState) 
{
       newDenoms: refreshDenoms,
     });
   } catch (e) {
-    if (e instanceof OperationFailedError) {
+    if (e instanceof TalerError) {
       console.log(e);
-      console.log(j2s(e.operationError));
+      console.log(j2s(e.errorDetail));
     } else {
       console.log(e);
     }
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
index 19668d76..0125b3b4 100644
--- 
a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
+++ 
b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts
@@ -63,7 +63,7 @@ export async function runWithdrawalAbortBankTest(t: 
GlobalTestState) {
   //
   // WHY ?!
   //
-  const e = await t.assertThrowsOperationErrorAsync(async () => {
+  const e = await t.assertThrowsTalerErrorAsync(async () => {
     await wallet.client.call(
       WalletApiOperation.AcceptBankIntegratedWithdrawal,
       {
@@ -73,7 +73,7 @@ export async function runWithdrawalAbortBankTest(t: 
GlobalTestState) {
     );
   });
   t.assertDeepEqual(
-    e.operationError.code,
+    e.errorDetail.code,
     TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
   );
 
diff --git a/packages/taler-wallet-core/src/bank-api-client.ts 
b/packages/taler-wallet-core/src/bank-api-client.ts
index 128e9a7a..14bf0717 100644
--- a/packages/taler-wallet-core/src/bank-api-client.ts
+++ b/packages/taler-wallet-core/src/bank-api-client.ts
@@ -31,7 +31,9 @@ import {
   getRandomBytes,
   j2s,
   Logger,
+  TalerErrorCode,
 } from "@gnu-taler/taler-util";
+import { TalerError } from "./errors.js";
 import {
   HttpRequestLibrary,
   readSuccessResponseJsonOrErrorCode,
@@ -104,15 +106,20 @@ export namespace BankApi {
     let paytoUri = `payto://x-taler-bank/localhost/${username}`;
     if (resp.status !== 200 && resp.status !== 202) {
       logger.error(`${j2s(await resp.json())}`);
-      throw new Error();
-    }
-    const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
-    // LibEuFin demobank returns payto URI in response
-    if (respJson.paytoUri) {
-      paytoUri = respJson.paytoUri;
+      throw TalerError.fromDetail(
+        TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+        {
+          httpStatusCode: resp.status,
+        },
+      );
     }
     try {
+      // Pybank has no body, thus this might throw.
       const respJson = await resp.json();
+      // LibEuFin demobank returns payto URI in response
+      if (respJson.paytoUri) {
+        paytoUri = respJson.paytoUri;
+      }
     } catch (e) {}
     return {
       password,
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index e9fe6a47..69606b8f 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -35,7 +35,7 @@ import {
   MerchantInfo,
   Product,
   RefreshReason,
-  TalerErrorDetails,
+  TalerErrorDetail,
   UnblindedSignature,
   CoinEnvelope,
   TalerProtocolTimestamp,
@@ -229,7 +229,7 @@ export interface ReserveRecord {
    * Last error that happened in a reserve operation
    * (either talking to the bank or the exchange).
    */
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
@@ -545,7 +545,7 @@ export interface ExchangeRecord {
    * Last error (if any) for fetching updated information about the
    * exchange.
    */
-  lastError?: TalerErrorDetails;
+  lastError?: TalerErrorDetail;
 
   /**
    * Retry status for fetching updated information about the exchange.
@@ -580,7 +580,7 @@ export interface PlanchetRecord {
 
   withdrawalDone: boolean;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 
   /**
    * Public key of the reserve that this planchet
@@ -820,14 +820,14 @@ export interface ProposalRecord {
    */
   retryInfo?: RetryInfo;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
  * Status of a tip we got from a merchant.
  */
 export interface TipRecord {
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 
   /**
    * Has the user accepted the tip?  Only after the tip has been accepted coins
@@ -922,9 +922,9 @@ export interface RefreshGroupRecord {
    */
   retryInfo: RetryInfo;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 
-  lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails };
+  lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetail };
 
   /**
    * Unique, randomly generated identifier for this group of
@@ -1256,7 +1256,7 @@ export interface PurchaseRecord {
 
   payRetryInfo?: RetryInfo;
 
-  lastPayError: TalerErrorDetails | undefined;
+  lastPayError: TalerErrorDetail | undefined;
 
   /**
    * Retry information for querying the refund status with the merchant.
@@ -1266,7 +1266,7 @@ export interface PurchaseRecord {
   /**
    * Last error (or undefined) for querying the refund status with the 
merchant.
    */
-  lastRefundStatusError: TalerErrorDetails | undefined;
+  lastRefundStatusError: TalerErrorDetail | undefined;
 
   /**
    * Continue querying the refund status until this deadline has expired.
@@ -1400,7 +1400,7 @@ export interface WithdrawalGroupRecord {
    */
   retryInfo: RetryInfo;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 export interface BankWithdrawUriRecord {
@@ -1465,7 +1465,7 @@ export interface RecoupGroupRecord {
   /**
    * Last error that occurred, if any.
    */
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 export enum BackupProviderStateTag {
@@ -1485,7 +1485,7 @@ export type BackupProviderState =
   | {
       tag: BackupProviderStateTag.Retrying;
       retryInfo: RetryInfo;
-      lastError?: TalerErrorDetails;
+      lastError?: TalerErrorDetail;
     };
 
 export interface BackupProviderTerms {
@@ -1598,7 +1598,7 @@ export interface DepositGroupRecord {
 
   operationStatus: OperationStatus;
 
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 
   /**
    * Retry info.
diff --git a/packages/taler-wallet-core/src/errors.ts 
b/packages/taler-wallet-core/src/errors.ts
index 3109644a..07a01a76 100644
--- a/packages/taler-wallet-core/src/errors.ts
+++ b/packages/taler-wallet-core/src/errors.ts
@@ -23,63 +23,143 @@
 /**
  * Imports.
  */
-import { TalerErrorCode, TalerErrorDetails } from "@gnu-taler/taler-util";
+import {
+  TalerErrorCode,
+  TalerErrorDetail,
+  TransactionType,
+} from "@gnu-taler/taler-util";
 
-/**
- * This exception is there to let the caller know that an error happened,
- * but the error has already been reported by writing it to the database.
- */
-export class OperationFailedAndReportedError extends Error {
-  static fromCode(
-    ec: TalerErrorCode,
-    message: string,
-    details: Record<string, unknown>,
-  ): OperationFailedAndReportedError {
-    return new OperationFailedAndReportedError(
-      makeErrorDetails(ec, message, details),
-    );
-  }
+export interface DetailsMap {
+  [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
+    innerError: TalerErrorDetail;
+    transactionId?: string;
+  };
+  [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: {
+    exchangeBaseUrl: string;
+  };
+  [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: {
+    exchangeProtocolVersion: string;
+    walletProtocolVersion: string;
+  };
+  [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: {};
+  [TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: {};
+  [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
+    orderId: string;
+    claimUrl: string;
+  };
+  [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: {};
+  [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
+    merchantPub: string;
+    orderId: string;
+  };
+  [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: {
+    baseUrlForDownload: string;
+    baseUrlFromContractTerms: string;
+  };
+  [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
+    talerPayUri: string;
+  };
+  [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {};
+  [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {};
+  [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {};
+  [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {};
+  [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {};
+  [TalerErrorCode.WALLET_NETWORK_ERROR]: {};
+  [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {};
+  [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: {};
+  [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
+  [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
+  [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
+}
 
-  constructor(public operationError: TalerErrorDetails) {
-    super(operationError.message);
+type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
 
-    // Set the prototype explicitly.
-    Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
-  }
+export function makeErrorDetail<C extends TalerErrorCode>(
+  code: C,
+  detail: ErrBody<C>,
+  hint?: string,
+): TalerErrorDetail {
+  // FIXME: include default hint?
+  return { code, hint, ...detail };
 }
 
-/**
- * This exception is thrown when an error occurred and the caller is
- * responsible for recording the failure in the database.
- */
-export class OperationFailedError extends Error {
-  static fromCode(
-    ec: TalerErrorCode,
-    message: string,
-    details: Record<string, unknown>,
-  ): OperationFailedError {
-    return new OperationFailedError(makeErrorDetails(ec, message, details));
+export function makePendingOperationFailedError(
+  innerError: TalerErrorDetail,
+  tag: TransactionType,
+  uid: string,
+): TalerError {
+  return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, 
{
+    innerError,
+    transactionId: `${tag}:${uid}`,
+  });
+}
+
+export class TalerError<T = any> extends Error {
+  errorDetail: TalerErrorDetail & T;
+  private constructor(d: TalerErrorDetail & T) {
+    super();
+    this.errorDetail = d;
+    Object.setPrototypeOf(this, TalerError.prototype);
   }
 
-  constructor(public operationError: TalerErrorDetails) {
-    super(operationError.message);
+  static fromDetail<C extends TalerErrorCode>(
+    code: C,
+    detail: ErrBody<C>,
+    hint?: string,
+  ): TalerError {
+    // FIXME: include default hint?
+    return new TalerError<unknown>({ code, hint, ...detail });
+  }
 
-    // Set the prototype explicitly.
-    Object.setPrototypeOf(this, OperationFailedError.prototype);
+  static fromUncheckedDetail(d: TalerErrorDetail): TalerError {
+    return new TalerError<unknown>({ ...d });
+  }
+
+  static fromException(e: any): TalerError {
+    const errDetail = getErrorDetailFromException(e);
+    return new TalerError(errDetail);
+  }
+
+  hasErrorCode<C extends keyof DetailsMap>(
+    code: C,
+  ): this is TalerError<DetailsMap[C]> {
+    return this.errorDetail.code === code;
   }
 }
 
-export function makeErrorDetails(
-  ec: TalerErrorCode,
-  message: string,
-  details: Record<string, unknown>,
-): TalerErrorDetails {
-  return {
-    code: ec,
-    hint: `Error: ${TalerErrorCode[ec]}`,
-    details: details,
-    message,
-  };
+/**
+ * Convert an exception (or anything that was thrown) into
+ * a TalerErrorDetail object.
+ */
+export function getErrorDetailFromException(e: any): TalerErrorDetail {
+  if (e instanceof TalerError) {
+    return e.errorDetail;
+  }
+  if (e instanceof Error) {
+    const err = makeErrorDetail(
+      TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+      {
+        stack: e.stack,
+      },
+      `unexpected exception (message: ${e.message})`,
+    );
+    return err;
+  }
+  // Something was thrown that is not even an exception!
+  // Try to stringify it.
+  let excString: string;
+  try {
+    excString = e.toString();
+  } catch (e) {
+    // Something went horribly wrong.
+    excString = "can't stringify exception";
+  }
+  const err = makeErrorDetail(
+    TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+    {},
+    `unexpected exception (not an exception, ${excString})`,
+  );
+  return err;
 }
 
 /**
@@ -89,44 +169,24 @@ export function makeErrorDetails(
  */
 export async function guardOperationException<T>(
   op: () => Promise<T>,
-  onOpError: (e: TalerErrorDetails) => Promise<void>,
+  onOpError: (e: TalerErrorDetail) => Promise<void>,
 ): Promise<T> {
   try {
     return await op();
   } catch (e: any) {
-    if (e instanceof OperationFailedAndReportedError) {
+    if (
+      e instanceof TalerError &&
+      e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED)
+    ) {
       throw e;
     }
-    if (e instanceof OperationFailedError) {
-      await onOpError(e.operationError);
-      throw new OperationFailedAndReportedError(e.operationError);
-    }
-    if (e instanceof Error) {
-      const opErr = makeErrorDetails(
-        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-        `unexpected exception (message: ${e.message})`,
-        {
-          stack: e.stack,
-        },
-      );
-      await onOpError(opErr);
-      throw new OperationFailedAndReportedError(opErr);
-    }
-    // Something was thrown that is not even an exception!
-    // Try to stringify it.
-    let excString: string;
-    try {
-      excString = e.toString();
-    } catch (e) {
-      // Something went horribly wrong.
-      excString = "can't stringify exception";
-    }
-    const opErr = makeErrorDetails(
-      TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-      `unexpected exception (not an exception, ${excString})`,
-      {},
-    );
+    const opErr = getErrorDetailFromException(e);
     await onOpError(opErr);
-    throw new OperationFailedAndReportedError(opErr);
+    throw TalerError.fromDetail(
+      TalerErrorCode.WALLET_PENDING_OPERATION_FAILED,
+      {
+        innerError: e.errorDetail,
+      },
+    );
   }
 }
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts 
b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
index 2a8c9e36..df25a109 100644
--- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
@@ -27,7 +27,7 @@ import {
 } from "../util/http.js";
 import { RequestThrottler } from "@gnu-taler/taler-util";
 import Axios, { AxiosResponse } from "axios";
-import { OperationFailedError, makeErrorDetails } from "../errors.js";
+import { TalerError } from "../errors.js";
 import { Logger, bytesToString } from "@gnu-taler/taler-util";
 import { TalerErrorCode, URL } from "@gnu-taler/taler-util";
 
@@ -55,14 +55,14 @@ export class NodeHttpLib implements HttpRequestLibrary {
 
     const parsedUrl = new URL(url);
     if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
-        `request to origin ${parsedUrl.origin} was throttled`,
         {
           requestMethod: method,
           requestUrl: url,
           throttleStats: this.throttle.getThrottleStats(url),
         },
+        `request to origin ${parsedUrl.origin} was throttled`,
       );
     }
     let timeout: number | undefined;
@@ -83,13 +83,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
         maxRedirects: 0,
       });
     } catch (e: any) {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_NETWORK_ERROR,
-        `${e.message}`,
         {
           requestUrl: url,
           requestMethod: method,
         },
+        `${e.message}`,
       );
     }
 
@@ -105,30 +105,26 @@ export class NodeHttpLib implements HttpRequestLibrary {
         responseJson = JSON.parse(respText);
       } catch (e) {
         logger.trace(`invalid json: '${resp.data}'`);
-        throw new OperationFailedError(
-          makeErrorDetails(
-            TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-            "invalid JSON",
-            {
-              httpStatusCode: resp.status,
-              requestUrl: url,
-              requestMethod: method,
-            },
-          ),
+        throw TalerError.fromDetail(
+          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+          {
+            httpStatusCode: resp.status,
+            requestUrl: url,
+            requestMethod: method,
+          },
+          "Could not parse response body as JSON",
         );
       }
       if (responseJson === null || typeof responseJson !== "object") {
         logger.trace(`invalid json (not an object): '${respText}'`);
-        throw new OperationFailedError(
-          makeErrorDetails(
-            TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-            "invalid JSON",
-            {
-              httpStatusCode: resp.status,
-              requestUrl: url,
-              requestMethod: method,
-            },
-          ),
+        throw TalerError.fromDetail(
+          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+          {
+            httpStatusCode: resp.status,
+            requestUrl: url,
+            requestMethod: method,
+          },
+          `invalid JSON`,
         );
       }
       return responseJson;
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index 48eea56a..400406ce 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -48,7 +48,7 @@ import {
   PreparePayResultType,
   RecoveryLoadRequest,
   RecoveryMergeStrategy,
-  TalerErrorDetails,
+  TalerErrorDetail,
   AbsoluteTime,
   URL,
   WalletBackupContentV1,
@@ -464,7 +464,7 @@ async function incrementBackupRetryInTx(
     backupProviders: typeof WalletStoresV1.backupProviders;
   }>,
   backupProviderBaseUrl: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   const pr = await tx.backupProviders.get(backupProviderBaseUrl);
   if (!pr) {
@@ -487,7 +487,7 @@ async function incrementBackupRetryInTx(
 async function incrementBackupRetry(
   ws: InternalWalletState,
   backupProviderBaseUrl: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ backupProviders: x.backupProviders }))
@@ -509,7 +509,7 @@ export async function processBackupForProvider(
     throw Error("unknown backup provider");
   }
 
-  const onOpErr = (err: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (err: TalerErrorDetail): Promise<void> =>
     incrementBackupRetry(ws, backupProviderBaseUrl, err);
 
   const run = async () => {
@@ -700,7 +700,7 @@ export interface ProviderInfo {
   /**
    * Last communication issue with the provider.
    */
-  lastError?: TalerErrorDetails;
+  lastError?: TalerErrorDetail;
   lastSuccessfulBackupTimestamp?: TalerProtocolTimestamp;
   lastAttemptedBackupTimestamp?: TalerProtocolTimestamp;
   paymentProposalIds: string[];
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 4b976011..42ce5e7c 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -36,7 +36,7 @@ import {
   Logger,
   NotificationType,
   parsePaytoUri,
-  TalerErrorDetails,
+  TalerErrorDetail,
   TalerProtocolTimestamp,
   TrackDepositGroupRequest,
   TrackDepositGroupResponse,
@@ -83,7 +83,7 @@ async function resetDepositGroupRetry(
 async function incrementDepositRetry(
   ws: InternalWalletState,
   depositGroupId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ depositGroups: x.depositGroups }))
@@ -111,7 +111,7 @@ export async function processDepositGroup(
   forceNow = false,
 ): Promise<void> {
   await ws.memoProcessDeposit.memo(depositGroupId, async () => {
-    const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+    const onOpErr = (e: TalerErrorDetail): Promise<void> =>
       incrementDepositRetry(ws, depositGroupId, e);
     return await guardOperationException(
       async () => await processDepositGroupImpl(ws, depositGroupId, forceNow),
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index df7eee76..bbed4228 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -34,7 +34,7 @@ import {
   Recoup,
   TalerErrorCode,
   URL,
-  TalerErrorDetails,
+  TalerErrorDetail,
   AbsoluteTime,
   hashDenomPub,
   LibtoolVersion,
@@ -64,11 +64,7 @@ import {
 } from "../util/http.js";
 import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
 import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
-import {
-  guardOperationException,
-  makeErrorDetails,
-  OperationFailedError,
-} from "../errors.js";
+import { guardOperationException, TalerError } from "../errors.js";
 import { InternalWalletState, TrustInfo } from "../common.js";
 import {
   WALLET_CACHE_BREAKER_CLIENT_VERSION,
@@ -112,7 +108,7 @@ function denominationRecordFromKeys(
 async function handleExchangeUpdateError(
   ws: InternalWalletState,
   baseUrl: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ exchanges: x.exchanges }))
@@ -353,7 +349,7 @@ export async function updateExchangeFromUrl(
   exchange: ExchangeRecord;
   exchangeDetails: ExchangeDetailsRecord;
 }> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (e: TalerErrorDetail): Promise<void> =>
     handleExchangeUpdateError(ws, baseUrl, e);
   return await guardOperationException(
     () => updateExchangeFromUrlImpl(ws, baseUrl, acceptedFormat, forceNow),
@@ -429,14 +425,13 @@ async function downloadExchangeKeysInfo(
   logger.info("received /keys response");
 
   if (exchangeKeysJsonUnchecked.denoms.length === 0) {
-    const opErr = makeErrorDetails(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
-      "exchange doesn't offer any denominations",
       {
         exchangeBaseUrl: baseUrl,
       },
+      "exchange doesn't offer any denominations",
     );
-    throw new OperationFailedError(opErr);
   }
 
   const protocolVersion = exchangeKeysJsonUnchecked.version;
@@ -446,15 +441,14 @@ async function downloadExchangeKeysInfo(
     protocolVersion,
   );
   if (versionRes?.compatible != true) {
-    const opErr = makeErrorDetails(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
-      "exchange protocol version not compatible with wallet",
       {
         exchangeProtocolVersion: protocolVersion,
         walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
       },
+      "exchange protocol version not compatible with wallet",
     );
-    throw new OperationFailedError(opErr);
   }
 
   const currency = Amounts.parseOrThrow(
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 9521d544..ce3a26c3 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -25,6 +25,7 @@
  * Imports.
  */
 import {
+  AbsoluteTime,
   AmountJson,
   Amounts,
   codecForContractTerms,
@@ -34,7 +35,6 @@ import {
   ConfirmPayResult,
   ConfirmPayResultType,
   ContractTerms,
-  decodeCrock,
   Duration,
   durationMax,
   durationMin,
@@ -43,19 +43,17 @@ import {
   getRandomBytes,
   HttpStatusCode,
   j2s,
-  kdf,
   Logger,
   NotificationType,
   parsePayUri,
   PreparePayResult,
   PreparePayResultType,
   RefreshReason,
-  stringToBytes,
   TalerErrorCode,
-  TalerErrorDetails,
-  AbsoluteTime,
-  URL,
+  TalerErrorDetail,
   TalerProtocolTimestamp,
+  TransactionType,
+  URL,
 } from "@gnu-taler/taler-util";
 import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js";
 import {
@@ -74,9 +72,9 @@ import {
 } from "../db.js";
 import {
   guardOperationException,
-  makeErrorDetails,
-  OperationFailedAndReportedError,
-  OperationFailedError,
+  makeErrorDetail,
+  makePendingOperationFailedError,
+  TalerError,
 } from "../errors.js";
 import {
   AvailableCoinInfo,
@@ -467,7 +465,7 @@ async function recordConfirmPay(
 async function reportProposalError(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ proposals: x.proposals }))
@@ -550,7 +548,7 @@ async function incrementPurchasePayRetry(
 async function reportPurchasePayError(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ purchases: x.purchases }))
@@ -575,7 +573,7 @@ export async function processDownloadProposal(
   proposalId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (err: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (err: TalerErrorDetail): Promise<void> =>
     reportProposalError(ws, proposalId, err);
   await guardOperationException(
     () => processDownloadProposalImpl(ws, proposalId, forceNow),
@@ -602,7 +600,7 @@ async function resetDownloadProposalRetry(
 async function failProposalPermanently(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ proposals: x.proposals }))
@@ -727,13 +725,13 @@ async function processDownloadProposalImpl(
   if (r.isError) {
     switch (r.talerErrorResponse.code) {
       case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED:
-        throw OperationFailedError.fromCode(
+        throw TalerError.fromDetail(
           TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
-          "order already claimed (likely by other wallet)",
           {
             orderId: proposal.orderId,
             claimUrl: orderClaimUrl,
           },
+          "order already claimed (likely by other wallet)",
         );
       default:
         throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
@@ -758,13 +756,17 @@ async function processDownloadProposalImpl(
     logger.trace(
       `malformed contract terms: ${j2s(proposalResp.contract_terms)}`,
     );
-    const err = makeErrorDetails(
+    const err = makeErrorDetail(
       TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
-      "validation for well-formedness failed",
       {},
+      "validation for well-formedness failed",
     );
     await failProposalPermanently(ws, proposalId, err);
-    throw new OperationFailedAndReportedError(err);
+    throw makePendingOperationFailedError(
+      err,
+      TransactionType.Payment,
+      proposalId,
+    );
   }
 
   const contractTermsHash = ContractTermsUtil.hashContractTerms(
@@ -780,13 +782,17 @@ async function processDownloadProposalImpl(
       proposalResp.contract_terms,
     );
   } catch (e) {
-    const err = makeErrorDetails(
+    const err = makeErrorDetail(
       TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
-      `schema validation failed: ${e}`,
       {},
+      `schema validation failed: ${e}`,
     );
     await failProposalPermanently(ws, proposalId, err);
-    throw new OperationFailedAndReportedError(err);
+    throw makePendingOperationFailedError(
+      err,
+      TransactionType.Payment,
+      proposalId,
+    );
   }
 
   const sigValid = await ws.cryptoApi.isValidContractTermsSignature(
@@ -796,16 +802,20 @@ async function processDownloadProposalImpl(
   );
 
   if (!sigValid) {
-    const err = makeErrorDetails(
+    const err = makeErrorDetail(
       TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID,
-      "merchant's signature on contract terms is invalid",
       {
         merchantPub: parsedContractTerms.merchant_pub,
         orderId: parsedContractTerms.order_id,
       },
+      "merchant's signature on contract terms is invalid",
     );
     await failProposalPermanently(ws, proposalId, err);
-    throw new OperationFailedAndReportedError(err);
+    throw makePendingOperationFailedError(
+      err,
+      TransactionType.Payment,
+      proposalId,
+    );
   }
 
   const fulfillmentUrl = parsedContractTerms.fulfillment_url;
@@ -814,16 +824,20 @@ async function processDownloadProposalImpl(
   const baseUrlFromContractTerms = parsedContractTerms.merchant_base_url;
 
   if (baseUrlForDownload !== baseUrlFromContractTerms) {
-    const err = makeErrorDetails(
+    const err = makeErrorDetail(
       TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH,
-      "merchant base URL mismatch",
       {
         baseUrlForDownload,
         baseUrlFromContractTerms,
       },
+      "merchant base URL mismatch",
     );
     await failProposalPermanently(ws, proposalId, err);
-    throw new OperationFailedAndReportedError(err);
+    throw makePendingOperationFailedError(
+      err,
+      TransactionType.Payment,
+      proposalId,
+    );
   }
 
   const contractData = extractContractData(
@@ -895,10 +909,8 @@ async function startDownloadProposal(
       ]);
     });
 
-  /**
-   * If we have already claimed this proposal with the same sessionId
-   * nonce and claim token, reuse it.
-   */
+  /* If we have already claimed this proposal with the same sessionId
+   * nonce and claim token, reuse it. */
   if (
     oldProposal &&
     oldProposal.downloadSessionId === sessionId &&
@@ -1029,7 +1041,7 @@ async function storePayReplaySuccess(
 async function handleInsufficientFunds(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   logger.trace("handling insufficient funds, trying to re-select coins");
 
@@ -1319,12 +1331,12 @@ export async function preparePayForUri(
   const uriResult = parsePayUri(talerPayUri);
 
   if (!uriResult) {
-    throw OperationFailedError.fromCode(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
-      `invalid taler://pay URI (${talerPayUri})`,
       {
         talerPayUri,
       },
+      `invalid taler://pay URI (${talerPayUri})`,
     );
   }
 
@@ -1503,7 +1515,7 @@ export async function processPurchasePay(
   proposalId: string,
   forceNow = false,
 ): Promise<ConfirmPayResult> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (e: TalerErrorDetail): Promise<void> =>
     reportPurchasePayError(ws, proposalId, e);
   return await guardOperationException(
     () => processPurchasePayImpl(ws, proposalId, forceNow),
@@ -1527,9 +1539,8 @@ async function processPurchasePayImpl(
       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: {},
+        hint: `trying to pay for purchase that is not in the database`,
+        proposalId: proposalId,
       },
     };
   }
@@ -1594,10 +1605,10 @@ async function processPurchasePayImpl(
       resp.status <= 599
     ) {
       logger.trace("treating /pay error as transient");
-      const err = makeErrorDetails(
+      const err = makeErrorDetail(
         TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/pay failed",
         getHttpResponseErrorDetails(resp),
+        "/pay failed",
       );
       return {
         type: ConfirmPayResultType.Pending,
@@ -1621,8 +1632,11 @@ async function processPurchasePayImpl(
           delete purch.payRetryInfo;
           await tx.purchases.put(purch);
         });
-      // FIXME: Maybe introduce a new return type for this instead of throwing?
-      throw new OperationFailedAndReportedError(errDetails);
+      throw makePendingOperationFailedError(
+        errDetails,
+        TransactionType.Payment,
+        proposalId,
+      );
     }
 
     if (resp.status === HttpStatusCode.Conflict) {
@@ -1692,10 +1706,10 @@ async function processPurchasePayImpl(
       resp.status >= 500 &&
       resp.status <= 599
     ) {
-      const err = makeErrorDetails(
+      const err = makeErrorDetail(
         TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/paid failed",
         getHttpResponseErrorDetails(resp),
+        "/paid failed",
       );
       return {
         type: ConfirmPayResultType.Pending,
@@ -1703,10 +1717,10 @@ async function processPurchasePayImpl(
       };
     }
     if (resp.status !== 204) {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-        "/paid failed",
         getHttpResponseErrorDetails(resp),
+        "/paid failed",
       );
     }
     await storePayReplaySuccess(ws, proposalId, sessionId);
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 84a27966..56c13f1b 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -30,7 +30,7 @@ import {
   j2s,
   NotificationType,
   RefreshReason,
-  TalerErrorDetails,
+  TalerErrorDetail,
   TalerProtocolTimestamp,
 } from "@gnu-taler/taler-util";
 import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
@@ -60,7 +60,7 @@ const logger = new Logger("operations/recoup.ts");
 async function incrementRecoupRetry(
   ws: InternalWalletState,
   recoupGroupId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({
@@ -384,7 +384,7 @@ export async function processRecoupGroup(
   forceNow = false,
 ): Promise<void> {
   await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
-    const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+    const onOpErr = (e: TalerErrorDetail): Promise<void> =>
       incrementRecoupRetry(ws, recoupGroupId, e);
     return await guardOperationException(
       async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 11f0f6c5..7753992f 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -43,7 +43,7 @@ import {
   NotificationType,
   RefreshGroupId,
   RefreshReason,
-  TalerErrorDetails,
+  TalerErrorDetail,
 } from "@gnu-taler/taler-util";
 import { AmountJson, Amounts } from "@gnu-taler/taler-util";
 import { amountToPretty } from "@gnu-taler/taler-util";
@@ -714,7 +714,7 @@ async function refreshReveal(
 async function incrementRefreshRetry(
   ws: InternalWalletState,
   refreshGroupId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({
@@ -747,7 +747,7 @@ export async function processRefreshGroup(
   forceNow = false,
 ): Promise<void> {
   await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
-    const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+    const onOpErr = (e: TalerErrorDetail): Promise<void> =>
       incrementRefreshRetry(ws, refreshGroupId, e);
     return await guardOperationException(
       async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index 686d545d..d888ff01 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -40,7 +40,7 @@ import {
   parseRefundUri,
   RefreshReason,
   TalerErrorCode,
-  TalerErrorDetails,
+  TalerErrorDetail,
   URL,
   codecForMerchantOrderStatusPaid,
   AbsoluteTime,
@@ -88,7 +88,7 @@ async function resetPurchaseQueryRefundRetry(
 async function incrementPurchaseQueryRefundRetry(
   ws: InternalWalletState,
   proposalId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({
@@ -592,7 +592,7 @@ export async function processPurchaseQueryRefund(
   proposalId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (e: TalerErrorDetail): Promise<void> =>
     incrementPurchaseQueryRefundRetry(ws, proposalId, e);
   await guardOperationException(
     () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true),
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts 
b/packages/taler-wallet-core/src/operations/reserves.ts
index ac948363..baa97703 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -34,7 +34,7 @@ import {
   NotificationType,
   randomBytes,
   TalerErrorCode,
-  TalerErrorDetails,
+  TalerErrorDetail,
   AbsoluteTime,
   URL,
 } from "@gnu-taler/taler-util";
@@ -47,7 +47,7 @@ import {
   WalletStoresV1,
   WithdrawalGroupRecord,
 } from "../db.js";
-import { guardOperationException, OperationFailedError } from "../errors.js";
+import { guardOperationException, TalerError } from "../errors.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import {
   readSuccessResponseJsonOrErrorCode,
@@ -135,7 +135,7 @@ async function incrementReserveRetry(
 async function reportReserveError(
   ws: InternalWalletState,
   reservePub: string,
-  err: TalerErrorDetails,
+  err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({
@@ -338,7 +338,7 @@ export async function processReserve(
   forceNow = false,
 ): Promise<void> {
   return ws.memoProcessReserve.memo(reservePub, async () => {
-    const onOpError = (err: TalerErrorDetails): Promise<void> =>
+    const onOpError = (err: TalerErrorDetail): Promise<void> =>
       reportReserveError(ws, reservePub, err);
     await guardOperationException(
       () => processReserveImpl(ws, reservePub, forceNow),
@@ -571,7 +571,7 @@ async function updateReserve(
     if (
       resp.status === 404 &&
       result.talerErrorResponse.code ===
-        TalerErrorCode.EXCHANGE_RESERVES_GET_STATUS_UNKNOWN
+        TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
     ) {
       ws.notify({
         type: NotificationType.ReserveNotYetFound,
@@ -803,9 +803,8 @@ export async function createTalerWithdrawReserve(
       return tx.reserves.get(reserve.reservePub);
     });
   if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
-    throw OperationFailedError.fromCode(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
-      "withdrawal aborted by bank",
       {},
     );
   }
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index 76512029..7b3d36a7 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -22,7 +22,7 @@ import {
   parseTipUri,
   codecForTipPickupGetResponse,
   Amounts,
-  TalerErrorDetails,
+  TalerErrorDetail,
   NotificationType,
   TipPlanchetDetail,
   TalerErrorCode,
@@ -44,7 +44,7 @@ import {
 import { j2s } from "@gnu-taler/taler-util";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
-import { guardOperationException, makeErrorDetails } from "../errors.js";
+import { guardOperationException, makeErrorDetail } from "../errors.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import { InternalWalletState } from "../common.js";
 import {
@@ -163,7 +163,7 @@ export async function prepareTip(
 async function incrementTipRetry(
   ws: InternalWalletState,
   walletTipId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({
@@ -192,7 +192,7 @@ export async function processTip(
   tipId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (e: TalerErrorDetail): Promise<void> =>
     incrementTipRetry(ws, tipId, e);
   await guardOperationException(
     () => processTipImpl(ws, tipId, forceNow),
@@ -296,10 +296,10 @@ async function processTipImpl(
       merchantResp.status === 424)
   ) {
     logger.trace(`got transient tip error`);
-    const err = makeErrorDetails(
+    const err = makeErrorDetail(
       TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-      "tip pickup failed (transient)",
       getHttpResponseErrorDetails(merchantResp),
+      "tip pickup failed (transient)",
     );
     await incrementTipRetry(ws, tipRecord.walletTipId, err);
     // FIXME: Maybe we want to signal to the caller that the transient error 
happened?
@@ -355,10 +355,10 @@ async function processTipImpl(
           if (!tipRecord) {
             return;
           }
-          tipRecord.lastError = makeErrorDetails(
+          tipRecord.lastError = makeErrorDetail(
             TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
-            "invalid signature from the exchange (via merchant tip) after 
unblinding",
             {},
+            "invalid signature from the exchange (via merchant tip) after 
unblinding",
           );
           await tx.tips.put(tipRecord);
         });
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index e4c6f2a6..1d7bf930 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -30,7 +30,7 @@ import {
   NotificationType,
   parseWithdrawUri,
   TalerErrorCode,
-  TalerErrorDetails,
+  TalerErrorDetail,
   AbsoluteTime,
   WithdrawResponse,
   URL,
@@ -42,6 +42,7 @@ import {
   ExchangeWithdrawRequest,
   Duration,
   TalerProtocolTimestamp,
+  TransactionType,
 } from "@gnu-taler/taler-util";
 import {
   CoinRecord,
@@ -63,9 +64,11 @@ import {
 } from "../util/http.js";
 import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
 import {
+  getErrorDetailFromException,
   guardOperationException,
-  makeErrorDetails,
-  OperationFailedError,
+  makeErrorDetail,
+  makePendingOperationFailedError,
+  TalerError,
 } from "../errors.js";
 import { InternalWalletState } from "../common.js";
 import {
@@ -299,15 +302,14 @@ export async function getBankWithdrawalInfo(
     config.version,
   );
   if (versionRes?.compatible != true) {
-    const opErr = makeErrorDetails(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
-      "bank integration protocol version not compatible with wallet",
       {
         exchangeProtocolVersion: config.version,
         walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
       },
+      "bank integration protocol version not compatible with wallet",
     );
-    throw new OperationFailedError(opErr);
   }
 
   const reqUrl = new URL(
@@ -526,12 +528,9 @@ async function processPlanchetExchangeRequest(
     );
     return r;
   } catch (e) {
+    const errDetail = getErrorDetailFromException(e);
     logger.trace("withdrawal request failed", e);
     logger.trace(e);
-    if (!(e instanceof OperationFailedError)) {
-      throw e;
-    }
-    const errDetails = e.operationError;
     await ws.db
       .mktx((x) => ({ planchets: x.planchets }))
       .runReadWrite(async (tx) => {
@@ -542,7 +541,7 @@ async function processPlanchetExchangeRequest(
         if (!planchet) {
           return;
         }
-        planchet.lastError = errDetails;
+        planchet.lastError = errDetail;
         await tx.planchets.put(planchet);
       });
     return;
@@ -628,10 +627,10 @@ async function processPlanchetVerifyAndStoreCoin(
         if (!planchet) {
           return;
         }
-        planchet.lastError = makeErrorDetails(
+        planchet.lastError = makeErrorDetail(
           TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
-          "invalid signature from the exchange after unblinding",
           {},
+          "invalid signature from the exchange after unblinding",
         );
         await tx.planchets.put(planchet);
       });
@@ -797,7 +796,7 @@ export async function updateWithdrawalDenoms(
 async function incrementWithdrawalRetry(
   ws: InternalWalletState,
   withdrawalGroupId: string,
-  err: TalerErrorDetails | undefined,
+  err: TalerErrorDetail | undefined,
 ): Promise<void> {
   await ws.db
     .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
@@ -821,7 +820,7 @@ export async function processWithdrawGroup(
   withdrawalGroupId: string,
   forceNow = false,
 ): Promise<void> {
-  const onOpErr = (e: TalerErrorDetails): Promise<void> =>
+  const onOpErr = (e: TalerErrorDetail): Promise<void> =>
     incrementWithdrawalRetry(ws, withdrawalGroupId, e);
   await guardOperationException(
     () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
@@ -919,7 +918,7 @@ async function processWithdrawGroupImpl(
 
   let numFinished = 0;
   let finishedForFirstTime = false;
-  let errorsPerCoin: Record<number, TalerErrorDetails> = {};
+  let errorsPerCoin: Record<number, TalerErrorDetail> = {};
 
   await ws.db
     .mktx((x) => ({
@@ -957,12 +956,12 @@ async function processWithdrawGroupImpl(
     });
 
   if (numFinished != numTotalCoins) {
-    throw OperationFailedError.fromCode(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
-      `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins 
withdrawn)`,
       {
         errorsPerCoin,
       },
+      `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins 
withdrawn)`,
     );
   }
 
diff --git a/packages/taler-wallet-core/src/pending-types.ts 
b/packages/taler-wallet-core/src/pending-types.ts
index 4b1434bb..f4e5216b 100644
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ b/packages/taler-wallet-core/src/pending-types.ts
@@ -25,7 +25,7 @@
  * Imports.
  */
 import {
-  TalerErrorDetails,
+  TalerErrorDetail,
   BalancesResponse,
   AbsoluteTime,
   TalerProtocolTimestamp,
@@ -71,7 +71,7 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
 export interface PendingBackupTask {
   type: PendingTaskType.Backup;
   backupProviderBaseUrl: string;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
@@ -80,7 +80,7 @@ export interface PendingBackupTask {
 export interface PendingExchangeUpdateTask {
   type: PendingTaskType.ExchangeUpdate;
   exchangeBaseUrl: string;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
@@ -124,7 +124,7 @@ export interface PendingReserveTask {
  */
 export interface PendingRefreshTask {
   type: PendingTaskType.Refresh;
-  lastError?: TalerErrorDetails;
+  lastError?: TalerErrorDetail;
   refreshGroupId: string;
   finishedPerCoin: boolean[];
   retryInfo: RetryInfo;
@@ -139,7 +139,7 @@ export interface PendingProposalDownloadTask {
   proposalTimestamp: TalerProtocolTimestamp;
   proposalId: string;
   orderId: string;
-  lastError?: TalerErrorDetails;
+  lastError?: TalerErrorDetail;
   retryInfo?: RetryInfo;
 }
 
@@ -173,7 +173,7 @@ export interface PendingPayTask {
   proposalId: string;
   isReplay: boolean;
   retryInfo?: RetryInfo;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
@@ -184,14 +184,14 @@ export interface PendingRefundQueryTask {
   type: PendingTaskType.RefundQuery;
   proposalId: string;
   retryInfo: RetryInfo;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 export interface PendingRecoupTask {
   type: PendingTaskType.Recoup;
   recoupGroupId: string;
   retryInfo: RetryInfo;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
 }
 
 /**
@@ -199,7 +199,7 @@ export interface PendingRecoupTask {
  */
 export interface PendingWithdrawTask {
   type: PendingTaskType.Withdraw;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
   retryInfo: RetryInfo;
   withdrawalGroupId: string;
 }
@@ -209,7 +209,7 @@ export interface PendingWithdrawTask {
  */
 export interface PendingDepositTask {
   type: PendingTaskType.Deposit;
-  lastError: TalerErrorDetails | undefined;
+  lastError: TalerErrorDetail | undefined;
   retryInfo: RetryInfo | undefined;
   depositGroupId: string;
 }
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 79afd570..31e38b60 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -24,16 +24,16 @@
 /**
  * Imports
  */
-import { OperationFailedError, makeErrorDetails } from "../errors.js";
 import {
   Logger,
   Duration,
   AbsoluteTime,
-  TalerErrorDetails,
+  TalerErrorDetail,
   Codec,
   j2s,
 } from "@gnu-taler/taler-util";
 import { TalerErrorCode } from "@gnu-taler/taler-util";
+import { makeErrorDetail, TalerError } from "../errors.js";
 
 const logger = new Logger("http.ts");
 
@@ -125,7 +125,7 @@ type ResponseOrError<T> =
 
 export async function readTalerErrorResponse(
   httpResponse: HttpResponse,
-): Promise<TalerErrorDetails> {
+): Promise<TalerErrorDetail> {
   const errJson = await httpResponse.json();
   const talerErrorCode = errJson.code;
   if (typeof talerErrorCode !== "number") {
@@ -134,16 +134,14 @@ export async function readTalerErrorResponse(
         errJson,
       )}`,
     );
-    throw new OperationFailedError(
-      makeErrorDetails(
-        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-        "Error response did not contain error code",
-        {
-          requestUrl: httpResponse.requestUrl,
-          requestMethod: httpResponse.requestMethod,
-          httpStatusCode: httpResponse.status,
-        },
-      ),
+    throw TalerError.fromDetail(
+      TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+      {
+        requestUrl: httpResponse.requestUrl,
+        requestMethod: httpResponse.requestMethod,
+        httpStatusCode: httpResponse.status,
+      },
+      "Error response did not contain error code",
     );
   }
   return errJson;
@@ -151,28 +149,28 @@ export async function readTalerErrorResponse(
 
 export async function readUnexpectedResponseDetails(
   httpResponse: HttpResponse,
-): Promise<TalerErrorDetails> {
+): Promise<TalerErrorDetail> {
   const errJson = await httpResponse.json();
   const talerErrorCode = errJson.code;
   if (typeof talerErrorCode !== "number") {
-    return makeErrorDetails(
+    return makeErrorDetail(
       TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-      "Error response did not contain error code",
       {
         requestUrl: httpResponse.requestUrl,
         requestMethod: httpResponse.requestMethod,
         httpStatusCode: httpResponse.status,
       },
+      "Error response did not contain error code",
     );
   }
-  return makeErrorDetails(
+  return makeErrorDetail(
     TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-    `Unexpected HTTP status (${httpResponse.status}) in response`,
     {
       requestUrl: httpResponse.requestUrl,
       httpStatusCode: httpResponse.status,
       errorResponse: errJson,
     },
+    `Unexpected HTTP status (${httpResponse.status}) in response`,
   );
 }
 
@@ -191,14 +189,14 @@ export async function 
readSuccessResponseJsonOrErrorCode<T>(
   try {
     parsedResponse = codec.decode(respJson);
   } catch (e: any) {
-    throw OperationFailedError.fromCode(
+    throw TalerError.fromDetail(
       TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-      "Response invalid",
       {
         requestUrl: httpResponse.requestUrl,
         httpStatusCode: httpResponse.status,
         validationError: e.toString(),
       },
+      "Response invalid",
     );
   }
   return {
@@ -220,16 +218,14 @@ export function throwUnexpectedRequestError(
   httpResponse: HttpResponse,
   talerErrorResponse: TalerErrorResponse,
 ): never {
-  throw new OperationFailedError(
-    makeErrorDetails(
-      TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
-      `Unexpected HTTP status ${httpResponse.status} in response`,
-      {
-        requestUrl: httpResponse.requestUrl,
-        httpStatusCode: httpResponse.status,
-        errorResponse: talerErrorResponse,
-      },
-    ),
+  throw TalerError.fromDetail(
+    TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+    {
+      requestUrl: httpResponse.requestUrl,
+      httpStatusCode: httpResponse.status,
+      errorResponse: talerErrorResponse,
+    },
+    `Unexpected HTTP status ${httpResponse.status} in response`,
   );
 }
 
@@ -251,16 +247,14 @@ export async function 
readSuccessResponseTextOrErrorCode<T>(
     const errJson = await httpResponse.json();
     const talerErrorCode = errJson.code;
     if (typeof talerErrorCode !== "number") {
-      throw new OperationFailedError(
-        makeErrorDetails(
-          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-          "Error response did not contain error code",
-          {
-            httpStatusCode: httpResponse.status,
-            requestUrl: httpResponse.requestUrl,
-            requestMethod: httpResponse.requestMethod,
-          },
-        ),
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+        {
+          httpStatusCode: httpResponse.status,
+          requestUrl: httpResponse.requestUrl,
+          requestMethod: httpResponse.requestMethod,
+        },
+        "Error response did not contain error code",
       );
     }
     return {
@@ -282,16 +276,14 @@ export async function checkSuccessResponseOrThrow(
     const errJson = await httpResponse.json();
     const talerErrorCode = errJson.code;
     if (typeof talerErrorCode !== "number") {
-      throw new OperationFailedError(
-        makeErrorDetails(
-          TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-          "Error response did not contain error code",
-          {
-            httpStatusCode: httpResponse.status,
-            requestUrl: httpResponse.requestUrl,
-            requestMethod: httpResponse.requestMethod,
-          },
-        ),
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+        {
+          httpStatusCode: httpResponse.status,
+          requestUrl: httpResponse.requestUrl,
+          requestMethod: httpResponse.requestMethod,
+        },
+        "Error response did not contain error code",
       );
     }
     throwUnexpectedRequestError(httpResponse, errJson);
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index bbff465a..cb8b53ad 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -100,9 +100,8 @@ import {
   WalletStoresV1,
 } from "./db.js";
 import {
-  makeErrorDetails,
-  OperationFailedAndReportedError,
-  OperationFailedError,
+  getErrorDetailFromException,
+  TalerError,
 } from "./errors.js";
 import { exportBackup } from "./operations/backup/export.js";
 import {
@@ -297,10 +296,10 @@ export async function runPending(
     try {
       await processOnePendingOperation(ws, p, forceNow);
     } catch (e) {
-      if (e instanceof OperationFailedAndReportedError) {
+      if (e instanceof TalerError) {
         console.error(
           "Operation failed:",
-          JSON.stringify(e.operationError, undefined, 2),
+          JSON.stringify(e.errorDetail, undefined, 2),
         );
       } else {
         console.error(e);
@@ -399,10 +398,16 @@ async function runTaskLoop(
         try {
           await processOnePendingOperation(ws, p);
         } catch (e) {
-          if (e instanceof OperationFailedAndReportedError) {
-            logger.warn("operation processed resulted in reported error");
-            logger.warn(`reported error was: ${j2s(e.operationError)}`);
+          if (
+            e instanceof TalerError &&
+            e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED)
+          ) {
+            logger.warn("operation processed resulted in error");
+            logger.warn(`error was: ${j2s(e.errorDetail)}`);
           } else {
+            // This is a bug, as we expect pending operations to always
+            // do their own error handling and only throw 
WALLET_PENDING_OPERATION_FAILED
+            // or return something.
             logger.error("Uncaught exception", e);
             ws.notify({
               type: NotificationType.InternalError,
@@ -722,7 +727,7 @@ export async function getClientFromWalletState(
       const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
       switch (res.type) {
         case "error":
-          throw new OperationFailedError(res.error);
+          throw TalerError.fromUncheckedDetail(res.error);
         case "response":
           return res.result;
       }
@@ -1040,12 +1045,12 @@ async function dispatchRequestInternal(
       return [];
     }
   }
-  throw OperationFailedError.fromCode(
+  throw TalerError.fromDetail(
     TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
-    "unknown operation",
     {
       operation,
     },
+    "unknown operation",
   );
 }
 
@@ -1067,34 +1072,13 @@ export async function handleCoreApiRequest(
       result,
     };
   } catch (e: any) {
-    if (
-      e instanceof OperationFailedError ||
-      e instanceof OperationFailedAndReportedError
-    ) {
-      logger.error("Caught operation failed error");
-      logger.trace((e as any).stack);
-      return {
-        type: "error",
-        operation,
-        id,
-        error: e.operationError,
-      };
-    } else {
-      try {
-        logger.error("Caught unexpected exception:");
-        logger.error(e.stack);
-      } catch (e) {}
-      return {
-        type: "error",
-        operation,
-        id,
-        error: makeErrorDetails(
-          TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-          `unexpected exception: ${e}`,
-          {},
-        ),
-      };
-    }
+    const err = getErrorDetailFromException(e);
+    return {
+      type: "error",
+      operation,
+      id,
+      error: err,
+    };
   }
 }
 
diff --git a/packages/taler-wallet-embedded/src/index.ts 
b/packages/taler-wallet-embedded/src/index.ts
index e01281bc..64b12f63 100644
--- a/packages/taler-wallet-embedded/src/index.ts
+++ b/packages/taler-wallet-embedded/src/index.ts
@@ -21,7 +21,6 @@ import {
   getDefaultNodeWallet,
   DefaultNodeWalletArgs,
   NodeHttpLib,
-  makeErrorDetails,
   handleWorkerError,
   handleWorkerMessage,
   HttpRequestLibrary,
@@ -33,6 +32,7 @@ import {
   WALLET_EXCHANGE_PROTOCOL_VERSION,
   WALLET_MERCHANT_PROTOCOL_VERSION,
   Wallet,
+  getErrorDetailFromException,
 } from "@gnu-taler/taler-wallet-core";
 
 import fs from "fs";
@@ -270,11 +270,7 @@ export function installNativeWalletListener(): void {
         type: "error",
         id,
         operation,
-        error: makeErrorDetails(
-          TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-          "unexpected exception",
-          {},
-        ),
+        error: getErrorDetailFromException(e),
       };
       sendNativeMessage(respMsg);
       return;
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts 
b/packages/taler-wallet-webextension/src/browserHttpLib.ts
index 8877edfc..53ab8559 100644
--- a/packages/taler-wallet-webextension/src/browserHttpLib.ts
+++ b/packages/taler-wallet-webextension/src/browserHttpLib.ts
@@ -18,11 +18,11 @@
  * Imports.
  */
 import {
-  OperationFailedError,
   HttpRequestLibrary,
   HttpRequestOptions,
   HttpResponse,
   Headers,
+  TalerError,
 } from "@gnu-taler/taler-wallet-core";
 import {
   Logger,
@@ -49,14 +49,14 @@ export class BrowserHttpLib implements HttpRequestLibrary {
 
     if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
       const parsedUrl = new URL(requestUrl);
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
-        `request to origin ${parsedUrl.origin} was throttled`,
         {
           requestMethod,
           requestUrl,
           throttleStats: this.throttle.getThrottleStats(requestUrl),
         },
+        `request to origin ${parsedUrl.origin} was throttled`,
       );
     }
 
@@ -78,12 +78,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
       myRequest.onerror = (e) => {
         logger.error("http request error");
         reject(
-          OperationFailedError.fromCode(
+          TalerError.fromDetail(
             TalerErrorCode.WALLET_NETWORK_ERROR,
-            "Could not make request",
             {
               requestUrl: requestUrl,
             },
+            "Could not make request",
           ),
         );
       };
@@ -91,12 +91,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
       myRequest.addEventListener("readystatechange", (e) => {
         if (myRequest.readyState === XMLHttpRequest.DONE) {
           if (myRequest.status === 0) {
-            const exc = OperationFailedError.fromCode(
+            const exc = TalerError.fromDetail(
               TalerErrorCode.WALLET_NETWORK_ERROR,
-              "HTTP request failed (status 0, maybe URI scheme was wrong?)",
               {
                 requestUrl: requestUrl,
               },
+              "HTTP request failed (status 0, maybe URI scheme was wrong?)",
             );
             reject(exc);
             return;
@@ -112,23 +112,23 @@ export class BrowserHttpLib implements HttpRequestLibrary 
{
               const responseString = td.decode(myRequest.response);
               responseJson = JSON.parse(responseString);
             } catch (e) {
-              throw OperationFailedError.fromCode(
+              throw TalerError.fromDetail(
                 TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-                "Invalid JSON from HTTP response",
                 {
                   requestUrl: requestUrl,
                   httpStatusCode: myRequest.status,
                 },
+                "Invalid JSON from HTTP response",
               );
             }
             if (responseJson === null || typeof responseJson !== "object") {
-              throw OperationFailedError.fromCode(
+              throw TalerError.fromDetail(
                 TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-                "Invalid JSON from HTTP response",
                 {
                   requestUrl: requestUrl,
                   httpStatusCode: myRequest.status,
                 },
+                "Invalid JSON from HTTP response",
               );
             }
             return responseJson;
diff --git 
a/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx 
b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
index 35670909..38d6ec56 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorTalerOperation.tsx
@@ -13,7 +13,7 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import { TalerErrorDetails } from "@gnu-taler/taler-util";
+import { TalerErrorDetail } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import arrowDown from "../../static/img/chevron-down.svg";
@@ -25,7 +25,7 @@ export function ErrorTalerOperation({
   error,
 }: {
   title?: VNode;
-  error?: TalerErrorDetails;
+  error?: TalerErrorDetail;
 }): VNode | null {
   const { devMode } = useDevContext();
   const [showErrorDetail, setShowErrorDetail] = useState(false);
@@ -59,7 +59,7 @@ export function ErrorTalerOperation({
         <Fragment>
           <div style={{ padding: 5, textAlign: "left" }}>
             <div>
-              <b>{error.message}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
+              <b>{error.hint}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
             </div>
           </div>
           {devMode && (
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.tsx 
b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
index ac6c1fd6..933195a9 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
@@ -35,7 +35,7 @@ import {
   PreparePayResultType,
   Translate,
 } from "@gnu-taler/taler-util";
-import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { ErrorTalerOperation } from "../components/ErrorTalerOperation";
@@ -64,9 +64,9 @@ export function DepositPage({ talerPayUri, goBack }: Props): 
VNode {
   const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
     undefined,
   );
-  const [payErrMsg, setPayErrMsg] = useState<
-    OperationFailedError | string | undefined
-  >(undefined);
+  const [payErrMsg, setPayErrMsg] = useState<TalerError | string | undefined>(
+    undefined,
+  );
 
   const balance = useAsyncAsHook(wxApi.getBalance, [
     NotificationType.CoinWithdrawn,
@@ -97,7 +97,7 @@ export function DepositPage({ talerPayUri, goBack }: Props): 
VNode {
         setPayStatus(p);
       } catch (e) {
         console.log("Got error while trying to pay", e);
-        if (e instanceof OperationFailedError) {
+        if (e instanceof TalerError) {
           setPayErrMsg(e);
         }
         if (e instanceof Error) {
@@ -117,7 +117,7 @@ export function DepositPage({ talerPayUri, goBack }: 
Props): VNode {
   }
 
   if (!payStatus) {
-    if (payErrMsg instanceof OperationFailedError) {
+    if (payErrMsg instanceof TalerError) {
       return (
         <WalletAction>
           <LogoHeader />
@@ -131,7 +131,7 @@ export function DepositPage({ talerPayUri, goBack }: 
Props): VNode {
                   Could not get the payment information for this order
                 </i18n.Translate>
               }
-              error={payErrMsg?.operationError}
+              error={payErrMsg?.errorDetail}
             />
           </section>
         </WalletAction>
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx 
b/packages/taler-wallet-webextension/src/cta/Pay.tsx
index 2a0d1b46..f6ae02f7 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx
@@ -36,7 +36,7 @@ import {
   PreparePayResultType,
   Product,
 } from "@gnu-taler/taler-util";
-import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { ErrorMessage } from "../components/ErrorMessage";
@@ -93,7 +93,7 @@ export function PayPage({
     undefined,
   );
   const [payErrMsg, setPayErrMsg] = useState<
-    OperationFailedError | string | undefined
+    TalerError | string | undefined
   >(undefined);
 
   const hook = useAsyncAsHook(async () => {
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
index 66c83cf1..d58e2ec8 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
@@ -25,10 +25,8 @@ import {
   AmountJson,
   Amounts,
   ExchangeListItem,
-  Translate,
   WithdrawUriInfoResponse,
 } from "@gnu-taler/taler-util";
-import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Loading } from "../components/Loading";
@@ -52,6 +50,7 @@ import {
 import * as wxApi from "../wxApi";
 import { TermsOfServiceSection } from "./TermsOfServiceSection";
 import { useTranslationContext } from "../context/translation";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 
 interface Props {
   talerWithdrawUri?: string;
@@ -85,9 +84,9 @@ export function View({
   reviewed,
 }: ViewProps): VNode {
   const { i18n } = useTranslationContext();
-  const [withdrawError, setWithdrawError] = useState<
-    OperationFailedError | undefined
-  >(undefined);
+  const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
+    undefined,
+  );
   const [confirmDisabled, setConfirmDisabled] = useState<boolean>(false);
 
   const needsReview = terms.status === "changed" || terms.status === "new";
@@ -109,7 +108,7 @@ export function View({
       setConfirmDisabled(true);
       await onWithdraw();
     } catch (e) {
-      if (e instanceof OperationFailedError) {
+      if (e instanceof TalerError) {
         setWithdrawError(e);
       }
       setConfirmDisabled(false);
@@ -130,7 +129,7 @@ export function View({
               Could not finish the withdrawal operation
             </i18n.Translate>
           }
-          error={withdrawError.operationError}
+          error={withdrawError.errorDetail}
         />
       )}
 
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts 
b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index 6efa1d18..8d31de94 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -13,28 +13,32 @@
  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
-import { NotificationType, TalerErrorDetails } from "@gnu-taler/taler-util";
+import {
+  NotificationType,
+  TalerErrorCode,
+  TalerErrorDetail,
+} from "@gnu-taler/taler-util";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { useEffect, useState } from "preact/hooks";
 import * as wxApi from "../wxApi";
-import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
 
 interface HookOk<T> {
   hasError: false;
   response: T;
 }
 
-export type HookError = HookGenericError | HookOperationalError
+export type HookError = HookGenericError | HookOperationalError;
 
 export interface HookGenericError {
   hasError: true;
-  operational: false,
+  operational: false;
   message: string;
 }
 
 export interface HookOperationalError {
   hasError: true;
-  operational: true,
-  details: TalerErrorDetails;
+  operational: true;
+  details: TalerErrorDetail;
 }
 
 export type HookResponse<T> = HookOk<T> | HookError | undefined;
@@ -51,10 +55,18 @@ export function useAsyncAsHook<T>(
         const response = await fn();
         setHookResponse({ hasError: false, response });
       } catch (e) {
-        if (e instanceof OperationFailedError) {
-          setHookResponse({ hasError: true, operational: true, details: 
e.operationError });
+        if (e instanceof TalerError) {
+          setHookResponse({
+            hasError: true,
+            operational: true,
+            details: e.errorDetail,
+          });
         } else if (e instanceof Error) {
-          setHookResponse({ hasError: true, operational: false, message: 
e.message });
+          setHookResponse({
+            hasError: true,
+            operational: false,
+            message: e.message,
+          });
         }
       }
     }
diff --git a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts 
b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
index 6f2585c1..4dee28a6 100644
--- a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
+++ b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
@@ -23,7 +23,7 @@ import {
   HttpRequestLibrary,
   HttpRequestOptions,
   HttpResponse,
-  OperationFailedError,
+  TalerError,
 } from "@gnu-taler/taler-wallet-core";
 
 /**
@@ -44,14 +44,14 @@ export class ServiceWorkerHttpLib implements 
HttpRequestLibrary {
 
     if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
       const parsedUrl = new URL(requestUrl);
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
-        `request to origin ${parsedUrl.origin} was throttled`,
         {
           requestMethod,
           requestUrl,
           throttleStats: this.throttle.getThrottleStats(requestUrl),
         },
+        `request to origin ${parsedUrl.origin} was throttled`,
       );
     }
 
@@ -107,13 +107,13 @@ function makeTextHandler(response: Response, requestUrl: 
string) {
     try {
       respText = await response.text();
     } catch (e) {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-        "Invalid JSON from HTTP response",
         {
           requestUrl,
           httpStatusCode: response.status,
         },
+        "Invalid JSON from HTTP response",
       );
     }
     return respText;
@@ -126,23 +126,23 @@ function makeJsonHandler(response: Response, requestUrl: 
string) {
     try {
       responseJson = await response.json();
     } catch (e) {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-        "Invalid JSON from HTTP response",
         {
           requestUrl,
           httpStatusCode: response.status,
         },
+        "Invalid JSON from HTTP response",
       );
     }
     if (responseJson === null || typeof responseJson !== "object") {
-      throw OperationFailedError.fromCode(
+      throw TalerError.fromDetail(
         TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
-        "Invalid JSON from HTTP response",
         {
           requestUrl,
           httpStatusCode: response.status,
         },
+        "Invalid JSON from HTTP response",
       );
     }
     return responseJson;
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts 
b/packages/taler-wallet-webextension/src/wxApi.ts
index 31b46d88..2071f85e 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -23,26 +23,48 @@
  */
 import {
   AcceptExchangeTosRequest,
-  AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
-  AddExchangeRequest, AmountString, ApplyRefundResponse, BalancesResponse, 
CoinDumpJson, ConfirmPayResult,
-  CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, 
DeleteTransactionRequest, ExchangesListRespose,
-  GetExchangeTosResult, GetExchangeWithdrawalInfo,
+  AcceptManualWithdrawalResult,
+  AcceptTipRequest,
+  AcceptWithdrawalResponse,
+  AddExchangeRequest,
+  AmountString,
+  ApplyRefundResponse,
+  BalancesResponse,
+  CoinDumpJson,
+  ConfirmPayResult,
+  CoreApiResponse,
+  CreateDepositGroupRequest,
+  CreateDepositGroupResponse,
+  DeleteTransactionRequest,
+  ExchangesListRespose,
+  GetExchangeTosResult,
+  GetExchangeWithdrawalInfo,
   GetFeeForDepositRequest,
-  GetWithdrawalDetailsForUriRequest, KnownBankAccounts, NotificationType, 
PreparePayResult, PrepareTipRequest,
-  PrepareTipResult, RetryTransactionRequest,
-  SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, 
WithdrawUriInfoResponse
+  GetWithdrawalDetailsForUriRequest,
+  KnownBankAccounts,
+  NotificationType,
+  PreparePayResult,
+  PrepareTipRequest,
+  PrepareTipResult,
+  RetryTransactionRequest,
+  SetWalletDeviceIdRequest,
+  TransactionsResponse,
+  WalletDiagnostics,
+  WithdrawUriInfoResponse,
 } from "@gnu-taler/taler-util";
 import {
-  AddBackupProviderRequest, BackupInfo, OperationFailedError,
+  AddBackupProviderRequest,
+  BackupInfo,
   PendingOperationsResponse,
-  RemoveBackupProviderRequest
+  RemoveBackupProviderRequest,
+  TalerError,
 } from "@gnu-taler/taler-wallet-core";
 import { DepositFee } from 
"@gnu-taler/taler-wallet-core/src/operations/deposits";
 import type { ExchangeWithdrawDetails } from 
"@gnu-taler/taler-wallet-core/src/operations/withdraw";
 import { MessageFromBackend } from "./wxBackend";
 
 /**
- * 
+ *
  * @autor Florian Dold
  * @autor sebasjm
  */
@@ -88,7 +110,7 @@ async function callBackend(operation: string, payload: any): 
Promise<any> {
       console.log("got response", resp);
       const r = resp as CoreApiResponse;
       if (r.type === "error") {
-        reject(new OperationFailedError(r.error));
+        reject(TalerError.fromUncheckedDetail(r.error));
         return;
       }
       resolve(r.result);
@@ -127,15 +149,23 @@ export function resetDb(): Promise<void> {
   return callBackend("reset-db", {});
 }
 
-export function getFeeForDeposit(depositPaytoUri: string, amount: 
AmountString): Promise<DepositFee> {
+export function getFeeForDeposit(
+  depositPaytoUri: string,
+  amount: AmountString,
+): Promise<DepositFee> {
   return callBackend("getFeeForDeposit", {
-    depositPaytoUri, amount
+    depositPaytoUri,
+    amount,
   } as GetFeeForDepositRequest);
 }
 
-export function createDepositGroup(depositPaytoUri: string, amount: 
AmountString): Promise<CreateDepositGroupResponse> {
+export function createDepositGroup(
+  depositPaytoUri: string,
+  amount: AmountString,
+): Promise<CreateDepositGroupResponse> {
   return callBackend("createDepositGroup", {
-    depositPaytoUri, amount
+    depositPaytoUri,
+    amount,
   } as CreateDepositGroupRequest);
 }
 
@@ -190,7 +220,9 @@ export function listKnownCurrencies(): 
Promise<ListOfKnownCurrencies> {
 export function listExchanges(): Promise<ExchangesListRespose> {
   return callBackend("listExchanges", {});
 }
-export function listKnownBankAccounts(currency?: string): 
Promise<KnownBankAccounts> {
+export function listKnownBankAccounts(
+  currency?: string,
+): Promise<KnownBankAccounts> {
   return callBackend("listKnownBankAccounts", { currency });
 }
 
@@ -387,14 +419,17 @@ export function exportDB(): Promise<any> {
 }
 
 export function importDB(dump: any): Promise<void> {
-  return callBackend("importDb", { dump })
+  return callBackend("importDb", { dump });
 }
 
-export function onUpdateNotification(messageTypes: Array<NotificationType>, 
doCallback: () => void): () => void {
+export function onUpdateNotification(
+  messageTypes: Array<NotificationType>,
+  doCallback: () => void,
+): () => void {
   // eslint-disable-next-line no-undef
   const port = chrome.runtime.connect({ name: "notifications" });
   const listener = (message: MessageFromBackend): void => {
-    const shouldNotify = messageTypes.includes(message.type)
+    const shouldNotify = messageTypes.includes(message.type);
     if (shouldNotify) {
       doCallback();
     }
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts 
b/packages/taler-wallet-webextension/src/wxBackend.ts
index e158d294..b7a0cdc5 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -35,7 +35,7 @@ import {
 import {
   DbAccess,
   deleteTalerDatabase,
-  makeErrorDetails,
+  makeErrorDetail,
   OpenedPromise,
   openPromise,
   openTalerDatabase,
@@ -167,10 +167,10 @@ async function dispatch(
           type: "error",
           id: req.id,
           operation: req.operation,
-          error: makeErrorDetails(
+          error: makeErrorDetail(
             TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
-            "wallet core not available",
             {},
+            "wallet core not available",
           ),
         };
         break;
@@ -233,7 +233,10 @@ function makeSyncWalletRedirect(
     const tab = await getTab(tabId);
     if (tab.url === oldUrl) {
       console.log("redirecting to", innerUrl.href);
-      chrome.tabs.update(tabId, { url: innerUrl.href, loadReplace: true } as 
any);
+      chrome.tabs.update(tabId, {
+        url: innerUrl.href,
+        loadReplace: true,
+      } as any);
     }
   };
   doit();

-- 
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]