gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: wallet-core: emit DD37 self-t


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: emit DD37 self-transition notifications with errors
Date: Tue, 20 Jun 2023 11:40:09 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 9c708251f wallet-core: emit DD37 self-transition notifications with 
errors
9c708251f is described below

commit 9c708251f92e6691ebba80fa8d129c6c04cec618
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Jun 20 11:40:06 2023 +0200

    wallet-core: emit DD37 self-transition notifications with errors
---
 .../test-withdrawal-bank-integrated.ts             |  51 +-
 packages/taler-util/src/notifications.ts           | 136 +----
 packages/taler-wallet-core/src/db.ts               |   2 +-
 .../taler-wallet-core/src/internal-wallet-state.ts |   9 +-
 .../src/operations/backup/import.ts                |  31 +-
 .../src/operations/backup/index.ts                 |  62 +--
 .../taler-wallet-core/src/operations/common.ts     | 581 ++++++++++++++++++---
 .../taler-wallet-core/src/operations/deposits.ts   |   4 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |  10 +-
 .../src/operations/pay-merchant.ts                 |  26 +-
 .../src/operations/pay-peer-common.ts              |   5 -
 .../src/operations/pay-peer-pull-credit.ts         |  27 +-
 .../src/operations/pay-peer-pull-debit.ts          |   9 +-
 .../src/operations/pay-peer-push-credit.ts         |   7 +-
 .../src/operations/pay-peer-push-debit.ts          |  31 +-
 .../taler-wallet-core/src/operations/pending.ts    |   2 +-
 .../taler-wallet-core/src/operations/recoup.ts     |   5 +-
 .../taler-wallet-core/src/operations/refresh.ts    |   8 +-
 packages/taler-wallet-core/src/operations/tip.ts   |   7 +-
 .../src/operations/transactions.ts                 |  26 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |  32 +-
 packages/taler-wallet-core/src/pending-types.ts    |   2 +-
 packages/taler-wallet-core/src/util/retries.ts     | 306 -----------
 packages/taler-wallet-core/src/wallet.ts           | 108 +++-
 24 files changed, 759 insertions(+), 728 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
 
b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
index c98c18db5..d0515d64f 100644
--- 
a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
+++ 
b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -24,7 +24,14 @@ import {
   BankApi,
   BankAccessApi,
 } from "@gnu-taler/taler-wallet-core";
-import { j2s, NotificationType, TransactionType, WithdrawalType } from 
"@gnu-taler/taler-util";
+import {
+  j2s,
+  NotificationType,
+  TransactionMajorState,
+  TransactionMinorState,
+  TransactionType,
+  WithdrawalType,
+} from "@gnu-taler/taler-util";
 
 /**
  * Run test for basic, bank-integrated withdrawal.
@@ -55,9 +62,22 @@ export async function runWithdrawalBankIntegratedTest(t: 
GlobalTestState) {
 
   // Withdraw
 
+  const r2 = await walletClient.client.call(
+    WalletApiOperation.AcceptBankIntegratedWithdrawal,
+    {
+      exchangeBaseUrl: exchange.baseUrl,
+      talerWithdrawUri: wop.taler_withdraw_uri,
+    },
+  );
+
   const withdrawalBankConfirmedCond = walletClient.waitForNotificationCond(
     (x) => {
-      return x.type === NotificationType.WithdrawalGroupBankConfirmed;
+      return (
+        x.type === NotificationType.TransactionStateTransition &&
+        x.transactionId === r2.transactionId &&
+        x.newTxState.major === TransactionMajorState.Pending &&
+        x.newTxState.minor === TransactionMinorState.ExchangeWaitReserve
+      );
     },
   );
 
@@ -67,15 +87,12 @@ export async function runWithdrawalBankIntegratedTest(t: 
GlobalTestState) {
 
   const withdrawalReserveReadyCond = walletClient.waitForNotificationCond(
     (x) => {
-      return x.type === NotificationType.WithdrawalGroupReserveReady;
-    },
-  );
-
-  const r2 = await walletClient.client.call(
-    WalletApiOperation.AcceptBankIntegratedWithdrawal,
-    {
-      exchangeBaseUrl: exchange.baseUrl,
-      talerWithdrawUri: wop.taler_withdraw_uri,
+      return (
+        x.type === NotificationType.TransactionStateTransition &&
+        x.transactionId === r2.transactionId &&
+        x.newTxState.major === TransactionMajorState.Pending &&
+        x.newTxState.minor === TransactionMinorState.WithdrawCoins
+      );
     },
   );
 
@@ -99,7 +116,9 @@ export async function runWithdrawalBankIntegratedTest(t: 
GlobalTestState) {
     console.log("transactions before confirmation:", j2s(txn));
     const tx0 = txn.transactions[0];
     t.assertTrue(tx0.type === TransactionType.Withdrawal);
-    t.assertTrue(tx0.withdrawalDetails.type === 
WithdrawalType.TalerBankIntegrationApi);
+    t.assertTrue(
+      tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+    );
     t.assertTrue(tx0.withdrawalDetails.confirmed === false);
     t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
   }
@@ -120,7 +139,9 @@ export async function runWithdrawalBankIntegratedTest(t: 
GlobalTestState) {
     console.log("transactions after confirmation:", j2s(txn));
     const tx0 = txn.transactions[0];
     t.assertTrue(tx0.type === TransactionType.Withdrawal);
-    t.assertTrue(tx0.withdrawalDetails.type === 
WithdrawalType.TalerBankIntegrationApi);
+    t.assertTrue(
+      tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+    );
     t.assertTrue(tx0.withdrawalDetails.confirmed === true);
     t.assertTrue(tx0.withdrawalDetails.reserveIsReady === false);
   }
@@ -138,7 +159,9 @@ export async function runWithdrawalBankIntegratedTest(t: 
GlobalTestState) {
     console.log("transactions after reserve ready:", j2s(txn));
     const tx0 = txn.transactions[0];
     t.assertTrue(tx0.type === TransactionType.Withdrawal);
-    t.assertTrue(tx0.withdrawalDetails.type === 
WithdrawalType.TalerBankIntegrationApi);
+    t.assertTrue(
+      tx0.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi,
+    );
     t.assertTrue(tx0.withdrawalDetails.confirmed === true);
     t.assertTrue(tx0.withdrawalDetails.reserveIsReady === true);
   }
diff --git a/packages/taler-util/src/notifications.ts 
b/packages/taler-util/src/notifications.ts
index 51b56c3fe..b05fea8c9 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -36,43 +36,31 @@ export enum NotificationType {
   RefreshMelted = "refresh-melted",
   RefreshStarted = "refresh-started",
   RefreshUnwarranted = "refresh-unwarranted",
-  ReserveUpdated = "reserve-updated",
-  ReserveConfirmed = "reserve-confirmed",
-  ReserveCreated = "reserve-created",
   WithdrawGroupCreated = "withdraw-group-created",
   WithdrawGroupFinished = "withdraw-group-finished",
   RefundStarted = "refund-started",
   RefundQueried = "refund-queried",
   ExchangeOperationError = "exchange-operation-error",
   ExchangeAdded = "exchange-added",
-  RefreshOperationError = "refresh-operation-error",
-  RecoupOperationError = "recoup-operation-error",
-  RefundApplyOperationError = "refund-apply-error",
-  RefundStatusOperationError = "refund-status-error",
-  ProposalOperationError = "proposal-error",
   BackupOperationError = "backup-error",
-  TipOperationError = "tip-error",
-  PayOperationError = "pay-error",
-  PayOperationSuccess = "pay-operation-success",
-  WithdrawOperationError = "withdraw-error",
-  ReserveNotYetFound = "reserve-not-yet-found",
-  ReserveOperationError = "reserve-error",
   InternalError = "internal-error",
   PendingOperationProcessed = "pending-operation-processed",
-  ProposalRefused = "proposal-refused",
-  ReserveRegisteredWithBank = "reserve-registered-with-bank",
   KycRequested = "kyc-requested",
-  WithdrawalGroupBankConfirmed = "withdrawal-group-bank-confirmed",
-  WithdrawalGroupReserveReady = "withdrawal-group-reserve-ready",
-  DepositOperationError = "deposit-operation-error",
   TransactionStateTransition = "transaction-state-transition",
 }
 
+export interface ErrorInfoSummary {
+  code: number;
+  hint?: string;
+  message?: string;
+}
+
 export interface TransactionStateTransitionNotification {
   type: NotificationType.TransactionStateTransition;
   transactionId: string;
   oldTxState: TransactionState;
   newTxState: TransactionState;
+  errorInfo?: ErrorInfoSummary;
 }
 
 export interface ProposalAcceptedNotification {
@@ -86,11 +74,6 @@ export interface InternalErrorNotification {
   exception: any;
 }
 
-export interface ReserveNotYetFoundNotification {
-  type: NotificationType.ReserveNotYetFound;
-  reservePub: string;
-}
-
 export interface CoinWithdrawnNotification {
   type: NotificationType.CoinWithdrawn;
   numWithdrawn: number;
@@ -137,16 +120,6 @@ export interface KycRequestedNotification {
   kycUrl: string;
 }
 
-export interface WithdrawalGroupBankConfirmed {
-  type: NotificationType.WithdrawalGroupBankConfirmed;
-  transactionId: string;
-}
-
-export interface WithdrawalGroupReserveReadyNotification {
-  type: NotificationType.WithdrawalGroupReserveReady;
-  transactionId: string;
-}
-
 export interface RefreshRevealedNotification {
   type: NotificationType.RefreshRevealed;
 }
@@ -159,10 +132,6 @@ export interface RefreshRefusedNotification {
   type: NotificationType.RefreshUnwarranted;
 }
 
-export interface ReserveConfirmedNotification {
-  type: NotificationType.ReserveConfirmed;
-}
-
 export interface WithdrawalGroupCreatedNotification {
   type: NotificationType.WithdrawGroupCreated;
   withdrawalGroupId: string;
@@ -182,103 +151,22 @@ export interface ExchangeOperationErrorNotification {
   error: TalerErrorDetail;
 }
 
-export interface RefreshOperationErrorNotification {
-  type: NotificationType.RefreshOperationError;
-  error: TalerErrorDetail;
-}
-
 export interface BackupOperationErrorNotification {
   type: NotificationType.BackupOperationError;
   error: TalerErrorDetail;
 }
 
-export interface RefundStatusOperationErrorNotification {
-  type: NotificationType.RefundStatusOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface RefundApplyOperationErrorNotification {
-  type: NotificationType.RefundApplyOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface PayOperationErrorNotification {
-  type: NotificationType.PayOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface ProposalOperationErrorNotification {
-  type: NotificationType.ProposalOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface TipOperationErrorNotification {
-  type: NotificationType.TipOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface WithdrawOperationErrorNotification {
-  type: NotificationType.WithdrawOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface RecoupOperationErrorNotification {
-  type: NotificationType.RecoupOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface DepositOperationErrorNotification {
-  type: NotificationType.DepositOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface ReserveOperationErrorNotification {
-  type: NotificationType.ReserveOperationError;
-  error: TalerErrorDetail;
-}
-
-export interface ReserveCreatedNotification {
-  type: NotificationType.ReserveCreated;
-  reservePub: string;
-}
 
 export interface PendingOperationProcessedNotification {
   type: NotificationType.PendingOperationProcessed;
   id: string;
 }
 
-export interface ProposalRefusedNotification {
-  type: NotificationType.ProposalRefused;
-}
-
-export interface ReserveRegisteredWithBankNotification {
-  type: NotificationType.ReserveRegisteredWithBank;
-}
-
-/**
- * Notification sent when a pay (or pay replay) operation succeeded.
- *
- * We send this notification because the confirmPay request can return
- * a "confirmed" response that indicates that the payment has been confirmed
- * by the user, but we're still waiting for the payment to succeed or fail.
- */
-export interface PayOperationSuccessNotification {
-  type: NotificationType.PayOperationSuccess;
-  proposalId: string;
-}
 
 export type WalletNotification =
   | BackupOperationErrorNotification
-  | WithdrawOperationErrorNotification
-  | ReserveOperationErrorNotification
   | ExchangeAddedNotification
   | ExchangeOperationErrorNotification
-  | RefreshOperationErrorNotification
-  | RefundStatusOperationErrorNotification
-  | RefundApplyOperationErrorNotification
-  | ProposalOperationErrorNotification
-  | PayOperationErrorNotification
-  | TipOperationErrorNotification
   | ProposalAcceptedNotification
   | ProposalDownloadedNotification
   | RefundsSubmittedNotification
@@ -288,22 +176,12 @@ export type WalletNotification =
   | RefreshRevealedNotification
   | RefreshStartedNotification
   | RefreshRefusedNotification
-  | ReserveCreatedNotification
-  | ReserveConfirmedNotification
   | WithdrawalGroupFinishedNotification
   | RefundStartedNotification
   | RefundQueriedNotification
   | WithdrawalGroupCreatedNotification
   | CoinWithdrawnNotification
-  | RecoupOperationErrorNotification
-  | DepositOperationErrorNotification
   | InternalErrorNotification
   | PendingOperationProcessedNotification
-  | ProposalRefusedNotification
-  | ReserveRegisteredWithBankNotification
-  | ReserveNotYetFoundNotification
-  | PayOperationSuccessNotification
   | KycRequestedNotification
-  | WithdrawalGroupBankConfirmed
-  | WithdrawalGroupReserveReadyNotification
   | TransactionStateTransitionNotification;
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 005b23985..3bf28aa94 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -70,7 +70,7 @@ import {
   StoreDescriptor,
   StoreWithIndexes,
 } from "./util/query.js";
-import { RetryInfo, TaskIdentifiers } from "./util/retries.js";
+import { RetryInfo, TaskIdentifiers } from "./operations/common.js";
 
 /**
  * This file contains the database schema of the Taler wallet together
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts 
b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 8dc83c65a..d97703dc1 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -35,6 +35,7 @@ import {
   DenominationInfo,
   RefreshGroupId,
   RefreshReason,
+  TransactionState,
   WalletNotification,
 } from "@gnu-taler/taler-util";
 import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
@@ -145,7 +146,7 @@ export interface ActiveLongpollInfo {
 }
 
 /**
- * Internal, shard wallet state that is used by the implementation
+ * Internal, shared wallet state that is used by the implementation
  * of wallet operations.
  *
  * FIXME:  This should not be exported anywhere from the taler-wallet-core 
package,
@@ -183,6 +184,12 @@ export interface InternalWalletState {
   merchantOps: MerchantOperations;
   refreshOps: RefreshOperations;
 
+  getTransactionState(
+    ws: InternalWalletState,
+    tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+    transactionId: string,
+  ): Promise<TransactionState | undefined>;
+
   getDenomInfo(
     ws: InternalWalletState,
     tx: GetReadOnlyAccess<{
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index cda5a012b..7f73a14b0 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -62,7 +62,7 @@ import { InternalWalletState } from 
"../../internal-wallet-state.js";
 import { assertUnreachable } from "../../util/assertUnreachable.js";
 import { checkLogicInvariant } from "../../util/invariants.js";
 import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import { makeCoinAvailable, makeTombstoneId, TombstoneTag } from 
"../common.js";
+import { constructTombstone, makeCoinAvailable, TombstoneTag } from 
"../common.js";
 import { getExchangeDetails } from "../exchanges.js";
 import { extractContractData } from "../pay-merchant.js";
 import { provideBackupState } from "./state.js";
@@ -472,7 +472,10 @@ export async function importBackup(
       for (const backupWg of backupBlob.withdrawal_groups) {
         const reservePub = cryptoComp.reservePrivToPub[backupWg.reserve_priv];
         checkLogicInvariant(!!reservePub);
-        const ts = makeTombstoneId(TombstoneTag.DeleteReserve, reservePub);
+        const ts = constructTombstone({
+          tag: TombstoneTag.DeleteReserve,
+          reservePub,
+        });
         if (tombstoneSet.has(ts)) {
           continue;
         }
@@ -558,10 +561,10 @@ export async function importBackup(
       }
 
       for (const backupPurchase of backupBlob.purchases) {
-        const ts = makeTombstoneId(
-          TombstoneTag.DeletePayment,
-          backupPurchase.proposal_id,
-        );
+        const ts = constructTombstone({
+          tag: TombstoneTag.DeletePayment,
+          proposalId: backupPurchase.proposal_id,
+        });
         if (tombstoneSet.has(ts)) {
           continue;
         }
@@ -704,10 +707,10 @@ export async function importBackup(
       }
 
       for (const backupRefreshGroup of backupBlob.refresh_groups) {
-        const ts = makeTombstoneId(
-          TombstoneTag.DeleteRefreshGroup,
-          backupRefreshGroup.refresh_group_id,
-        );
+        const ts = constructTombstone({
+          tag: TombstoneTag.DeleteRefreshGroup,
+          refreshGroupId: backupRefreshGroup.refresh_group_id,
+        });
         if (tombstoneSet.has(ts)) {
           continue;
         }
@@ -800,10 +803,10 @@ export async function importBackup(
       }
 
       for (const backupTip of backupBlob.tips) {
-        const ts = makeTombstoneId(
-          TombstoneTag.DeleteTip,
-          backupTip.wallet_tip_id,
-        );
+        const ts = constructTombstone({
+          tag: TombstoneTag.DeleteTip,
+          walletTipId: backupTip.wallet_tip_id,
+        });
         if (tombstoneSet.has(ts)) {
           continue;
         }
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index f726167da..364e876ec 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -29,52 +29,52 @@ import {
   AmountString,
   AttentionType,
   BackupRecovery,
+  Codec,
+  DenomKeyType,
+  EddsaKeyPair,
+  HttpStatusCode,
+  Logger,
+  PreparePayResult,
+  PreparePayResultType,
+  RecoveryLoadRequest,
+  RecoveryMergeStrategy,
+  TalerError,
+  TalerErrorCode,
+  TalerErrorDetail,
+  TalerPreciseTimestamp,
+  URL,
+  WalletBackupContentV1,
   buildCodecForObject,
   buildCodecForUnion,
   bytesToString,
-  canonicalizeBaseUrl,
   canonicalJson,
-  Codec,
+  canonicalizeBaseUrl,
   codecForAmountString,
   codecForBoolean,
   codecForConstString,
   codecForList,
   codecForNumber,
   codecForString,
-  codecForTalerErrorDetail,
   codecOptional,
-  ConfirmPayResultType,
   decodeCrock,
-  DenomKeyType,
   durationFromSpec,
   eddsaGetPublic,
-  EddsaKeyPair,
   encodeCrock,
   getRandomBytes,
   hash,
   hashDenomPub,
-  HttpStatusCode,
   j2s,
   kdf,
-  Logger,
   notEmpty,
-  PaymentStatus,
-  PreparePayResult,
-  PreparePayResultType,
-  RecoveryLoadRequest,
-  RecoveryMergeStrategy,
-  ReserveTransactionType,
   rsaBlind,
   secretbox,
   secretbox_open,
   stringToBytes,
-  TalerErrorCode,
-  TalerErrorDetail,
-  TalerProtocolTimestamp,
-  TalerPreciseTimestamp,
-  URL,
-  WalletBackupContentV1,
 } from "@gnu-taler/taler-util";
+import {
+  readSuccessResponseJsonOrThrow,
+  readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
 import { gunzipSync, gzipSync } from "fflate";
 import { TalerCryptoInterface } from "../../crypto/cryptoImplementation.js";
 import {
@@ -86,29 +86,19 @@ import {
   ConfigRecordKey,
   WalletBackupConfState,
 } from "../../db.js";
-import { TalerError } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../../internal-wallet-state.js";
 import { assertUnreachable } from "../../util/assertUnreachable.js";
-import {
-  readSuccessResponseJsonOrThrow,
-  readTalerErrorResponse,
-} from "@gnu-taler/taler-util/http";
 import {
   checkDbInvariant,
   checkLogicInvariant,
 } from "../../util/invariants.js";
+import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
 import {
   OperationAttemptResult,
   OperationAttemptResultType,
   TaskIdentifiers,
-  scheduleRetryInTx,
-} from "../../util/retries.js";
-import { addAttentionRequest, removeAttentionRequest } from "../attention.js";
-import {
-  checkPaymentByProposalId,
-  confirmPay,
-  preparePayForUri,
-} from "../pay-merchant.js";
+} from "../common.js";
+import { checkPaymentByProposalId, preparePayForUri } from 
"../pay-merchant.js";
 import { exportBackup } from "./export.js";
 import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
 import { getWalletBackupState, provideBackupState } from "./state.js";
@@ -380,8 +370,6 @@ async function runBackupCycleForProvider(
             logger.warn("backup provider not found anymore");
             return;
           }
-          const opId = TaskIdentifiers.forBackup(prov);
-          await scheduleRetryInTx(ws, tx, opId);
           prov.shouldRetryFreshProposal = true;
           prov.state = {
             tag: BackupProviderStateTag.Retrying,
@@ -407,7 +395,7 @@ async function runBackupCycleForProvider(
           return;
         }
         const opId = TaskIdentifiers.forBackup(prov);
-        await scheduleRetryInTx(ws, tx, opId);
+        //await scheduleRetryInTx(ws, tx, opId);
         prov.currentPaymentProposalId = result.proposalId;
         prov.shouldRetryFreshProposal = false;
         prov.state = {
@@ -481,7 +469,7 @@ async function runBackupCycleForProvider(
         // FIXME: Allocate error code for this situation?
         // FIXME: Add operation retry record!
         const opId = TaskIdentifiers.forBackup(prov);
-        await scheduleRetryInTx(ws, tx, opId);
+        //await scheduleRetryInTx(ws, tx, opId);
         prov.state = {
           tag: BackupProviderStateTag.Retrying,
         };
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index ad18767c4..293870a18 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -18,42 +18,56 @@
  * Imports.
  */
 import {
+  AbsoluteTime,
   AgeRestriction,
   AmountJson,
   Amounts,
   CancellationToken,
   CoinRefreshRequest,
   CoinStatus,
+  Duration,
+  ErrorInfoSummary,
   ExchangeEntryStatus,
   ExchangeListItem,
   ExchangeTosStatus,
   getErrorDetailFromException,
   j2s,
   Logger,
+  NotificationType,
   OperationErrorInfo,
   RefreshReason,
   TalerErrorCode,
   TalerErrorDetail,
   TombstoneIdStr,
   TransactionIdStr,
+  TransactionType,
+  WalletNotification,
 } from "@gnu-taler/taler-util";
 import {
   WalletStoresV1,
   CoinRecord,
   ExchangeDetailsRecord,
   ExchangeRecord,
+  BackupProviderRecord,
+  DepositGroupRecord,
+  PeerPullPaymentIncomingRecord,
+  PeerPullPaymentInitiationRecord,
+  PeerPushPaymentIncomingRecord,
+  PeerPushPaymentInitiationRecord,
+  PurchaseRecord,
+  RecoupGroupRecord,
+  RefreshGroupRecord,
+  TipRecord,
+  WithdrawalGroupRecord,
 } from "../db.js";
 import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import {
-  OperationAttemptResult,
-  OperationAttemptResultType,
-  RetryInfo,
-} from "../util/retries.js";
+import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
 import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
-import { TaskId } from "../pending-types.js";
+import { PendingTaskType, TaskId } from "../pending-types.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { constructTransactionIdentifier } from "./transactions.js";
 
 const logger = new Logger("operations/common.ts");
 
@@ -197,68 +211,185 @@ export async function spendCoins(
   );
 }
 
-export async function storeOperationError(
+/**
+ * Convert the task ID for a task that processes a transaction int
+ * the ID for the transaction.
+ */
+function convertTaskToTransactionId(
+  taskId: string,
+): TransactionIdStr | undefined {
+  const parsedTaskId = parseTaskIdentifier(taskId);
+  switch (parsedTaskId.tag) {
+    case PendingTaskType.PeerPullCredit:
+      return constructTransactionIdentifier({
+        tag: TransactionType.PeerPullCredit,
+        pursePub: parsedTaskId.pursePub,
+      });
+    case PendingTaskType.PeerPullDebit:
+      return constructTransactionIdentifier({
+        tag: TransactionType.PeerPullDebit,
+        peerPullPaymentIncomingId: parsedTaskId.peerPullPaymentIncomingId,
+      });
+    // FIXME: This doesn't distinguish internal-withdrawal.
+    // Maybe we should have a different task type for that as well?
+    // Or maybe transaction IDs should be valid task identifiers?
+    case PendingTaskType.Withdraw:
+      return constructTransactionIdentifier({
+        tag: TransactionType.Withdrawal,
+        withdrawalGroupId: parsedTaskId.withdrawalGroupId,
+      });
+    case PendingTaskType.PeerPushCredit:
+      return constructTransactionIdentifier({
+        tag: TransactionType.PeerPushCredit,
+        peerPushPaymentIncomingId: parsedTaskId.peerPushPaymentIncomingId,
+      });
+    case PendingTaskType.Deposit:
+      return constructTransactionIdentifier({
+        tag: TransactionType.Deposit,
+        depositGroupId: parsedTaskId.depositGroupId,
+      });
+    case PendingTaskType.Refresh:
+      return constructTransactionIdentifier({
+        tag: TransactionType.Refresh,
+        refreshGroupId: parsedTaskId.refreshGroupId,
+      });
+    case PendingTaskType.TipPickup:
+      return constructTransactionIdentifier({
+        tag: TransactionType.Tip,
+        walletTipId: parsedTaskId.walletTipId,
+      });
+    case PendingTaskType.PeerPushDebit:
+      return constructTransactionIdentifier({
+        tag: TransactionType.PeerPushDebit,
+        pursePub: parsedTaskId.pursePub,
+      });
+    case PendingTaskType.Purchase:
+      return constructTransactionIdentifier({
+        tag: TransactionType.Payment,
+        proposalId: parsedTaskId.proposalId,
+      });
+    default:
+      return undefined;
+  }
+}
+
+/**
+ * For tasks that process a transaction,
+ * generate a state transition notification.
+ */
+async function taskToTransactionNotification(
+  ws: InternalWalletState,
+  tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+  pendingTaskId: string,
+  e: TalerErrorDetail | undefined,
+): Promise<WalletNotification | undefined> {
+  const txId = convertTaskToTransactionId(pendingTaskId);
+  if (!txId) {
+    return undefined;
+  }
+  const txState = await ws.getTransactionState(ws, tx, txId);
+  if (!txState) {
+    return undefined;
+  }
+  const notif: WalletNotification = {
+    type: NotificationType.TransactionStateTransition,
+    transactionId: txId,
+    oldTxState: txState,
+    newTxState: txState,
+  };
+  if (e) {
+    notif.errorInfo = {
+      code: e.code as number,
+      hint: e.hint,
+    };
+  }
+  return notif;
+}
+
+async function storePendingTaskError(
   ws: InternalWalletState,
   pendingTaskId: string,
   e: TalerErrorDetail,
 ): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      let retryRecord = await tx.operationRetries.get(pendingTaskId);
-      if (!retryRecord) {
-        retryRecord = {
-          id: pendingTaskId,
-          lastError: e,
-          retryInfo: RetryInfo.reset(),
-        };
-      } else {
-        retryRecord.lastError = e;
-        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
-      }
+  logger.info(`storing pending task error for ${pendingTaskId}`);
+  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+    let retryRecord = await tx.operationRetries.get(pendingTaskId);
+    if (!retryRecord) {
+      retryRecord = {
+        id: pendingTaskId,
+        lastError: e,
+        retryInfo: RetryInfo.reset(),
+      };
+    } else {
+      retryRecord.lastError = e;
+      retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+    }
+    await tx.operationRetries.put(retryRecord);
+    return taskToTransactionNotification(ws, tx, pendingTaskId, e);
+  });
+  if (maybeNotification) {
+    ws.notify(maybeNotification);
+  }
+}
+
+export async function resetPendingTaskTimeout(
+  ws: InternalWalletState,
+  pendingTaskId: string,
+): Promise<void> {
+  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+    let retryRecord = await tx.operationRetries.get(pendingTaskId);
+    if (retryRecord) {
+      // Note that we don't reset the lastError, it should still be visible
+      // while the retry runs.
+      retryRecord.retryInfo = RetryInfo.reset();
       await tx.operationRetries.put(retryRecord);
-    });
+    }
+    return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+  });
+  if (maybeNotification) {
+    ws.notify(maybeNotification);
+  }
 }
 
-export async function resetOperationTimeout(
+async function storePendingTaskPending(
   ws: InternalWalletState,
   pendingTaskId: string,
 ): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      let retryRecord = await tx.operationRetries.get(pendingTaskId);
-      if (retryRecord) {
-        // Note that we don't reset the lastError, it should still be visible
-        // while the retry runs.
-        retryRecord.retryInfo = RetryInfo.reset();
-        await tx.operationRetries.put(retryRecord);
+  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
+    let retryRecord = await tx.operationRetries.get(pendingTaskId);
+    let hadError = false;
+    if (!retryRecord) {
+      retryRecord = {
+        id: pendingTaskId,
+        retryInfo: RetryInfo.reset(),
+      };
+    } else {
+      if (retryRecord.lastError) {
+        hadError = true;
       }
-    });
+      delete retryRecord.lastError;
+      retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+    }
+    await tx.operationRetries.put(retryRecord);
+    return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
+  });
+  if (maybeNotification) {
+    ws.notify(maybeNotification);
+  }
 }
 
-export async function storeOperationPending(
+async function storePendingTaskFinished(
   ws: InternalWalletState,
   pendingTaskId: string,
 ): Promise<void> {
   await ws.db
     .mktx((x) => [x.operationRetries])
     .runReadWrite(async (tx) => {
-      let retryRecord = await tx.operationRetries.get(pendingTaskId);
-      if (!retryRecord) {
-        retryRecord = {
-          id: pendingTaskId,
-          retryInfo: RetryInfo.reset(),
-        };
-      } else {
-        delete retryRecord.lastError;
-        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
-      }
-      await tx.operationRetries.put(retryRecord);
+      await tx.operationRetries.delete(pendingTaskId);
     });
 }
 
-export async function runOperationWithErrorReporting<T1, T2>(
+export async function runTaskWithErrorReporting<T1, T2>(
   ws: InternalWalletState,
   opId: TaskId,
   f: () => Promise<OperationAttemptResult<T1, T2>>,
@@ -268,13 +399,13 @@ export async function runOperationWithErrorReporting<T1, 
T2>(
     const resp = await f();
     switch (resp.type) {
       case OperationAttemptResultType.Error:
-        await storeOperationError(ws, opId, resp.errorDetail);
+        await storePendingTaskError(ws, opId, resp.errorDetail);
         return resp;
       case OperationAttemptResultType.Finished:
-        await storeOperationFinished(ws, opId);
+        await storePendingTaskFinished(ws, opId);
         return resp;
       case OperationAttemptResultType.Pending:
-        await storeOperationPending(ws, opId);
+        await storePendingTaskPending(ws, opId);
         return resp;
       case OperationAttemptResultType.Longpoll:
         return resp;
@@ -297,7 +428,7 @@ export async function runOperationWithErrorReporting<T1, 
T2>(
       logger.warn("operation processed resulted in error");
       logger.warn(`error was: ${j2s(e.errorDetail)}`);
       maybeError = e.errorDetail;
-      await storeOperationError(ws, opId, maybeError!);
+      await storePendingTaskError(ws, opId, maybeError!);
       return {
         type: OperationAttemptResultType.Error,
         errorDetail: e.errorDetail,
@@ -315,7 +446,7 @@ export async function runOperationWithErrorReporting<T1, 
T2>(
         },
         `unexpected exception (message: ${e.message})`,
       );
-      await storeOperationError(ws, opId, maybeError);
+      await storePendingTaskError(ws, opId, maybeError);
       return {
         type: OperationAttemptResultType.Error,
         errorDetail: maybeError,
@@ -327,7 +458,7 @@ export async function runOperationWithErrorReporting<T1, 
T2>(
         {},
         `unexpected exception (not even an error)`,
       );
-      await storeOperationError(ws, opId, maybeError);
+      await storePendingTaskError(ws, opId, maybeError);
       return {
         type: OperationAttemptResultType.Error,
         errorDetail: maybeError,
@@ -336,17 +467,6 @@ export async function runOperationWithErrorReporting<T1, 
T2>(
   }
 }
 
-export async function storeOperationFinished(
-  ws: InternalWalletState,
-  pendingTaskId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      await tx.operationRetries.delete(pendingTaskId);
-    });
-}
-
 export enum TombstoneTag {
   DeleteWithdrawalGroup = "delete-withdrawal-group",
   DeleteReserve = "delete-reserve",
@@ -361,15 +481,6 @@ export enum TombstoneTag {
   DeletePeerPushCredit = "delete-peer-push-credit",
 }
 
-/**
- * Create an event ID from the type and the primary key for the event.
- *
- * @deprecated use constructTombstone instead
- */
-export function makeTombstoneId(type: TombstoneTag, ...args: string[]): string 
{
-  return `tmb:${type}:${args.map((x) => encodeURIComponent(x)).join(":")}`;
-}
-
 export function getExchangeTosStatus(
   exchangeDetails: ExchangeDetailsRecord,
 ): ExchangeTosStatus {
@@ -432,7 +543,7 @@ export function runLongpollAsync(
   const asyncFn = async () => {
     if (ws.stopped) {
       logger.trace("not long-polling reserve, wallet already stopped");
-      await storeOperationPending(ws, retryTag);
+      await storePendingTaskPending(ws, retryTag);
       return;
     }
     const cts = CancellationToken.create();
@@ -446,13 +557,13 @@ export function runLongpollAsync(
       };
       res = await reqFn(cts.token);
     } catch (e) {
-      await storeOperationError(ws, retryTag, getErrorDetailFromException(e));
+      await storePendingTaskError(ws, retryTag, 
getErrorDetailFromException(e));
       return;
     } finally {
       delete ws.activeLongpoll[retryTag];
     }
     if (!res.ready) {
-      await storeOperationPending(ws, retryTag);
+      await storePendingTaskPending(ws, retryTag);
     }
     ws.workAvailable.trigger();
   };
@@ -464,7 +575,11 @@ export type ParsedTombstone =
       tag: TombstoneTag.DeleteWithdrawalGroup;
       withdrawalGroupId: string;
     }
-  | { tag: TombstoneTag.DeleteRefund; refundGroupId: string };
+  | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
+  | { tag: TombstoneTag.DeleteReserve; reservePub: string }
+  | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
+  | { tag: TombstoneTag.DeleteTip; walletTipId: string }
+  | { tag: TombstoneTag.DeletePayment; proposalId: string };
 
 export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
   switch (p.tag) {
@@ -472,6 +587,16 @@ export function constructTombstone(p: ParsedTombstone): 
TombstoneIdStr {
       return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
     case TombstoneTag.DeleteRefund:
       return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr;
+    case TombstoneTag.DeleteReserve:
+      return `tmb:${p.tag}:${p.reservePub}` as TombstoneIdStr;
+    case TombstoneTag.DeletePayment:
+      return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
+    case TombstoneTag.DeleteRefreshGroup:
+      return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
+    case TombstoneTag.DeleteTip:
+      return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
+    default:
+      assertUnreachable(p);
   }
 }
 
@@ -487,3 +612,305 @@ export interface TransactionManager {
   resume(): Promise<void>;
   process(): Promise<OperationAttemptResult>;
 }
+
+export enum OperationAttemptResultType {
+  Finished = "finished",
+  Pending = "pending",
+  Error = "error",
+  Longpoll = "longpoll",
+}
+
+export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
+  | OperationAttemptFinishedResult<TSuccess>
+  | OperationAttemptErrorResult
+  | OperationAttemptLongpollResult
+  | OperationAttemptPendingResult<TPending>;
+
+export namespace OperationAttemptResult {
+  export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
+    return {
+      type: OperationAttemptResultType.Finished,
+      result: undefined,
+    };
+  }
+  export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
+    return {
+      type: OperationAttemptResultType.Pending,
+      result: undefined,
+    };
+  }
+  export function longpoll(): OperationAttemptResult<unknown, unknown> {
+    return {
+      type: OperationAttemptResultType.Longpoll,
+    };
+  }
+}
+
+export interface OperationAttemptFinishedResult<T> {
+  type: OperationAttemptResultType.Finished;
+  result: T;
+}
+
+export interface OperationAttemptPendingResult<T> {
+  type: OperationAttemptResultType.Pending;
+  result: T;
+}
+
+export interface OperationAttemptErrorResult {
+  type: OperationAttemptResultType.Error;
+  errorDetail: TalerErrorDetail;
+}
+
+export interface OperationAttemptLongpollResult {
+  type: OperationAttemptResultType.Longpoll;
+}
+
+export interface RetryInfo {
+  firstTry: AbsoluteTime;
+  nextRetry: AbsoluteTime;
+  retryCounter: number;
+}
+
+export interface RetryPolicy {
+  readonly backoffDelta: Duration;
+  readonly backoffBase: number;
+  readonly maxTimeout: Duration;
+}
+
+const defaultRetryPolicy: RetryPolicy = {
+  backoffBase: 1.5,
+  backoffDelta: Duration.fromSpec({ seconds: 1 }),
+  maxTimeout: Duration.fromSpec({ minutes: 2 }),
+};
+
+function updateTimeout(
+  r: RetryInfo,
+  p: RetryPolicy = defaultRetryPolicy,
+): void {
+  const now = AbsoluteTime.now();
+  if (now.t_ms === "never") {
+    throw Error("assertion failed");
+  }
+  if (p.backoffDelta.d_ms === "forever") {
+    r.nextRetry = AbsoluteTime.never();
+    return;
+  }
+
+  const nextIncrement =
+    p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
+
+  const t =
+    now.t_ms +
+    (p.maxTimeout.d_ms === "forever"
+      ? nextIncrement
+      : Math.min(p.maxTimeout.d_ms, nextIncrement));
+  r.nextRetry = AbsoluteTime.fromMilliseconds(t);
+}
+
+export namespace RetryInfo {
+  export function getDuration(
+    r: RetryInfo | undefined,
+    p: RetryPolicy = defaultRetryPolicy,
+  ): Duration {
+    if (!r) {
+      // If we don't have any retry info, run immediately.
+      return { d_ms: 0 };
+    }
+    if (p.backoffDelta.d_ms === "forever") {
+      return { d_ms: "forever" };
+    }
+    const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
+    return {
+      d_ms:
+        p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
+    };
+  }
+
+  export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
+    const now = AbsoluteTime.now();
+    const info = {
+      firstTry: now,
+      nextRetry: now,
+      retryCounter: 0,
+    };
+    updateTimeout(info, p);
+    return info;
+  }
+
+  export function increment(
+    r: RetryInfo | undefined,
+    p: RetryPolicy = defaultRetryPolicy,
+  ): RetryInfo {
+    if (!r) {
+      return reset(p);
+    }
+    const r2 = { ...r };
+    r2.retryCounter++;
+    updateTimeout(r2, p);
+    return r2;
+  }
+}
+
+/**
+ * Parsed representation of task identifiers.
+ */
+export type ParsedTaskIdentifier =
+  | {
+      tag: PendingTaskType.Withdraw;
+      withdrawalGroupId: string;
+    }
+  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+  | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
+  | { tag: PendingTaskType.Deposit; depositGroupId: string }
+  | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
+  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
+  | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
+  | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
+  | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
+  | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
+  | { tag: PendingTaskType.Purchase; proposalId: string }
+  | { tag: PendingTaskType.Recoup; recoupGroupId: string }
+  | { tag: PendingTaskType.TipPickup; walletTipId: string }
+  | { tag: PendingTaskType.Refresh; refreshGroupId: string };
+
+export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
+  const task = x.split(":");
+
+  if (task.length < 2) {
+    throw Error("task id should have al least 2 parts separated by ':'");
+  }
+
+  const [type, ...rest] = task;
+  switch (type) {
+    case PendingTaskType.Backup:
+      return { tag: type, backupProviderBaseUrl: rest[0] };
+    case PendingTaskType.Deposit:
+      return { tag: type, depositGroupId: rest[0] };
+    case PendingTaskType.ExchangeCheckRefresh:
+      return { tag: type, exchangeBaseUrl: rest[0] };
+    case PendingTaskType.ExchangeUpdate:
+      return { tag: type, exchangeBaseUrl: rest[0] };
+    case PendingTaskType.PeerPullCredit:
+      return { tag: type, pursePub: rest[0] };
+    case PendingTaskType.PeerPullDebit:
+      return { tag: type, peerPullPaymentIncomingId: rest[0] };
+    case PendingTaskType.PeerPushCredit:
+      return { tag: type, peerPushPaymentIncomingId: rest[0] };
+    case PendingTaskType.PeerPushDebit:
+      return { tag: type, pursePub: rest[0] };
+    case PendingTaskType.Purchase:
+      return { tag: type, proposalId: rest[0] };
+    case PendingTaskType.Recoup:
+      return { tag: type, recoupGroupId: rest[0] };
+    case PendingTaskType.Refresh:
+      return { tag: type, refreshGroupId: rest[0] };
+    case PendingTaskType.TipPickup:
+      return { tag: type, walletTipId: rest[0] };
+    case PendingTaskType.Withdraw:
+      return { tag: type, withdrawalGroupId: rest[0] };
+    default:
+      throw Error("invalid task identifier");
+  }
+}
+
+export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
+  switch (p.tag) {
+    case PendingTaskType.Backup:
+      return `${p.tag}:${p.backupProviderBaseUrl}` as TaskId;
+    case PendingTaskType.Deposit:
+      return `${p.tag}:${p.depositGroupId}` as TaskId;
+    case PendingTaskType.ExchangeCheckRefresh:
+      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
+    case PendingTaskType.ExchangeUpdate:
+      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
+    case PendingTaskType.PeerPullDebit:
+      return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId;
+    case PendingTaskType.PeerPushCredit:
+      return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId;
+    case PendingTaskType.PeerPullCredit:
+      return `${p.tag}:${p.pursePub}` as TaskId;
+    case PendingTaskType.PeerPushDebit:
+      return `${p.tag}:${p.pursePub}` as TaskId;
+    case PendingTaskType.Purchase:
+      return `${p.tag}:${p.proposalId}` as TaskId;
+    case PendingTaskType.Recoup:
+      return `${p.tag}:${p.recoupGroupId}` as TaskId;
+    case PendingTaskType.Refresh:
+      return `${p.tag}:${p.refreshGroupId}` as TaskId;
+    case PendingTaskType.TipPickup:
+      return `${p.tag}:${p.walletTipId}` as TaskId;
+    case PendingTaskType.Withdraw:
+      return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
+    default:
+      assertUnreachable(p);
+  }
+}
+
+export namespace TaskIdentifiers {
+  export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
+    return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
+  }
+  export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
+    return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
+  }
+  export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
+    return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
+  }
+  export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
+    return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
+  }
+  export function forTipPickup(tipRecord: TipRecord): TaskId {
+    return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
+  }
+  export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
+    return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` 
as TaskId;
+  }
+  export function forPay(purchaseRecord: PurchaseRecord): TaskId {
+    return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as 
TaskId;
+  }
+  export function forRecoup(recoupRecord: RecoupGroupRecord): TaskId {
+    return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskId;
+  }
+  export function forDeposit(depositRecord: DepositGroupRecord): TaskId {
+    return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as 
TaskId;
+  }
+  export function forBackup(backupRecord: BackupProviderRecord): TaskId {
+    return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
+  }
+  export function forPeerPushPaymentInitiation(
+    ppi: PeerPushPaymentInitiationRecord,
+  ): TaskId {
+    return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
+  }
+  export function forPeerPullPaymentInitiation(
+    ppi: PeerPullPaymentInitiationRecord,
+  ): TaskId {
+    return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
+  }
+  export function forPeerPullPaymentDebit(
+    ppi: PeerPullPaymentIncomingRecord,
+  ): TaskId {
+    return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` 
as TaskId;
+  }
+  export function forPeerPushCredit(
+    ppi: PeerPushPaymentIncomingRecord,
+  ): TaskId {
+    return 
`${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId;
+  }
+}
+
+/**
+ * Run an operation handler, expect a success result and extract the success 
value.
+ */
+export async function unwrapOperationHandlerResultOrThrow<T>(
+  res: OperationAttemptResult<T>,
+): Promise<T> {
+  switch (res.type) {
+    case OperationAttemptResultType.Finished:
+      return res.result;
+    case OperationAttemptResultType.Error:
+      throw TalerError.fromUncheckedDetail(res.errorDetail);
+    default:
+      throw Error(`unexpected operation result (${res.type})`);
+  }
+}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 64180a3ea..6781696cf 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -79,8 +79,7 @@ import {
 } from "../index.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { OperationAttemptResult } from "../util/retries.js";
-import { spendCoins, TombstoneTag } from "./common.js";
+import { constructTaskIdentifier, OperationAttemptResult, spendCoins, 
TombstoneTag } from "./common.js";
 import { getExchangeDetails } from "./exchanges.js";
 import {
   extractContractData,
@@ -94,7 +93,6 @@ import {
   parseTransactionIdentifier,
   stopLongpolling,
 } from "./transactions.js";
-import { constructTaskIdentifier } from "../util/retries.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 
 /**
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index 40ef22c6d..7e01071b4 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -74,14 +74,8 @@ import {
   GetReadOnlyAccess,
   GetReadWriteAccess,
 } from "../util/query.js";
-import {
-  OperationAttemptResult,
-  OperationAttemptResultType,
-  TaskIdentifiers,
-  unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
 import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
-import { runOperationWithErrorReporting } from "./common.js";
+import { OperationAttemptResult, OperationAttemptResultType, 
runTaskWithErrorReporting, TaskIdentifiers, unwrapOperationHandlerResultOrThrow 
} from "./common.js";
 
 const logger = new Logger("exchanges.ts");
 
@@ -559,7 +553,7 @@ export async function updateExchangeFromUrl(
 }> {
   const canonUrl = canonicalizeBaseUrl(baseUrl);
   return unwrapOperationHandlerResultOrThrow(
-    await runOperationWithErrorReporting(
+    await runTaskWithErrorReporting(
       ws,
       TaskIdentifiers.forExchangeUpdateFromUrl(canonUrl),
       () => updateExchangeFromUrlHandler(ws, canonUrl, options),
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts 
b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index c3f288ff7..ad6552f06 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -67,7 +67,6 @@ import {
   TalerErrorCode,
   TalerErrorDetail,
   TalerPreciseTimestamp,
-  TalerProtocolTimestamp,
   TalerProtocolViolationError,
   TalerUriAction,
   TransactionAction,
@@ -116,12 +115,11 @@ import {
   OperationAttemptResult,
   OperationAttemptResultType,
   RetryInfo,
-  scheduleRetry,
   TaskIdentifiers,
-} from "../util/retries.js";
+} from "./common.js";
 import {
   runLongpollAsync,
-  runOperationWithErrorReporting,
+  runTaskWithErrorReporting,
   spendCoins,
 } from "./common.js";
 import {
@@ -1254,7 +1252,7 @@ export async function runPayForConfirmPay(
     tag: PendingTaskType.Purchase,
     proposalId,
   });
-  const res = await runOperationWithErrorReporting(ws, taskId, async () => {
+  const res = await runTaskWithErrorReporting(ws, taskId, async () => {
     return await processPurchasePay(ws, proposalId, { forceNow: true });
   });
   logger.trace(`processPurchasePay response type ${res.type}`);
@@ -1618,18 +1616,11 @@ export async function processPurchasePay(
         // Do this in the background, as it might take some time
         handleInsufficientFunds(ws, proposalId, err).catch(async (e) => {
           console.log("handling insufficient funds failed");
-
-          await scheduleRetry(ws, TaskIdentifiers.forPay(purchase), {
-            code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-            when: AbsoluteTime.now(),
-            message: "unexpected exception",
-            hint: "unexpected exception",
-            details: {
-              exception: e.toString(),
-            },
-          });
+          console.log(`${e.toString()}`);
         });
 
+        // FIXME: Should we really consider this to be pending?
+
         return {
           type: OperationAttemptResultType.Pending,
           result: undefined,
@@ -1694,11 +1685,6 @@ export async function processPurchasePay(
     await unblockBackup(ws, proposalId);
   }
 
-  ws.notify({
-    type: NotificationType.PayOperationSuccess,
-    proposalId: purchase.proposalId,
-  });
-
   return OperationAttemptResult.finishedEmpty();
 }
 
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 4856fbe36..1bc2e8d49 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -52,11 +52,6 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { checkDbInvariant } from "../util/invariants.js";
 import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
 import { getTotalRefreshCost } from "./refresh.js";
-import {
-  OperationAttemptLongpollResult,
-  OperationAttemptResult,
-  OperationAttemptResultType,
-} from "../util/retries.js";
 
 const logger = new Logger("operations/peer-to-peer.ts");
 
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
index 48b81d6c2..5baba8cdc 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts
@@ -66,12 +66,9 @@ import {
   OperationAttemptResult,
   OperationAttemptResultType,
   constructTaskIdentifier,
-} from "../util/retries.js";
-import {
   LongpollResult,
-  resetOperationTimeout,
   runLongpollAsync,
-  runOperationWithErrorReporting,
+  runTaskWithErrorReporting,
 } from "./common.js";
 import {
   codecForExchangePurseStatus,
@@ -486,26 +483,6 @@ export async function processPeerPullCredit(
 
   switch (pullIni.status) {
     case PeerPullPaymentInitiationStatus.Done: {
-      // We implement this case so that the "retry" action on a 
peer-pull-credit transaction
-      // also retries the withdrawal task.
-
-      logger.warn(
-        "peer pull payment initiation is already finished, retrying 
withdrawal",
-      );
-
-      const withdrawalGroupId = pullIni.withdrawalGroupId;
-
-      if (withdrawalGroupId) {
-        const taskId = constructTaskIdentifier({
-          tag: PendingTaskType.Withdraw,
-          withdrawalGroupId,
-        });
-        stopLongpolling(ws, taskId);
-        await resetOperationTimeout(ws, taskId);
-        await runOperationWithErrorReporting(ws, taskId, () =>
-          processWithdrawalGroup(ws, withdrawalGroupId),
-        );
-      }
       return {
         type: OperationAttemptResultType.Finished,
         result: undefined,
@@ -811,7 +788,7 @@ export async function initiatePeerPullPayment(
     pursePub: pursePair.pub,
   });
 
-  await runOperationWithErrorReporting(ws, taskId, async () => {
+  await runTaskWithErrorReporting(ws, taskId, async () => {
     return processPeerPullCredit(ws, pursePair.pub);
   });
 
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 0595a9e67..322d9ca71 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -59,13 +59,15 @@ import {
   createRefreshGroup,
 } from "../index.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
+import { checkLogicInvariant } from "../util/invariants.js";
 import {
   OperationAttemptResult,
   OperationAttemptResultType,
   TaskIdentifiers,
   constructTaskIdentifier,
-} from "../util/retries.js";
-import { runOperationWithErrorReporting, spendCoins } from "./common.js";
+  runTaskWithErrorReporting,
+  spendCoins,
+} from "./common.js";
 import {
   PeerCoinRepair,
   codecForExchangePurseStatus,
@@ -78,7 +80,6 @@ import {
   notifyTransition,
   stopLongpolling,
 } from "./transactions.js";
-import { checkLogicInvariant } from "../util/invariants.js";
 
 const logger = new Logger("pay-peer-pull-debit.ts");
 
@@ -462,7 +463,7 @@ export async function confirmPeerPullDebit(
       return pi;
     });
 
-  await runOperationWithErrorReporting(
+  await runTaskWithErrorReporting(
     ws,
     TaskIdentifiers.forPeerPullPaymentDebit(ppi),
     async () => {
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
index 9b563b37e..cf698d512 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts
@@ -60,12 +60,7 @@ import {
 } from "../index.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import { checkDbInvariant } from "../util/invariants.js";
-import {
-  OperationAttemptResult,
-  OperationAttemptResultType,
-  constructTaskIdentifier,
-} from "../util/retries.js";
-import { runLongpollAsync } from "./common.js";
+import { OperationAttemptResult, OperationAttemptResultType, 
constructTaskIdentifier, runLongpollAsync } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   codecForExchangePurseStatus,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
index fc7e868dc..c4209eb51 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -42,40 +42,41 @@ import {
   j2s,
   stringifyTalerUri,
 } from "@gnu-taler/taler-util";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import {
-  selectPeerCoins,
-  getTotalPeerPaymentCost,
-  codecForExchangePurseStatus,
-  queryCoinInfosForSelection,
-  PeerCoinRepair,
-} from "./pay-peer-common.js";
 import {
   HttpResponse,
   readSuccessResponseJsonOrThrow,
   readTalerErrorResponse,
 } from "@gnu-taler/taler-util/http";
+import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
 import {
   PeerPushPaymentInitiationRecord,
   PeerPushPaymentInitiationStatus,
   RefreshOperationStatus,
   createRefreshGroup,
 } from "../index.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
 import { PendingTaskType } from "../pending-types.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { checkLogicInvariant } from "../util/invariants.js";
 import {
   OperationAttemptResult,
   OperationAttemptResultType,
   constructTaskIdentifier,
-} from "../util/retries.js";
-import { runLongpollAsync, spendCoins } from "./common.js";
+  runLongpollAsync,
+  spendCoins,
+} from "./common.js";
+import {
+  PeerCoinRepair,
+  codecForExchangePurseStatus,
+  getTotalPeerPaymentCost,
+  queryCoinInfosForSelection,
+  selectPeerCoins,
+} from "./pay-peer-common.js";
 import {
   constructTransactionIdentifier,
   notifyTransition,
   stopLongpolling,
 } from "./transactions.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
-import { checkLogicInvariant } from "../util/invariants.js";
-import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
 
 const logger = new Logger("pay-peer-push-debit.ts");
 
@@ -162,10 +163,10 @@ async function handlePurseCreationConflict(
         case PeerPushPaymentInitiationStatus.PendingCreatePurse:
         case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
           const sel = coinSelRes.result;
-          myPpi.coinSel =  {
+          myPpi.coinSel = {
             coinPubs: sel.coins.map((x) => x.coinPub),
             contributions: sel.coins.map((x) => x.contribution),
-          }
+          };
           break;
         }
         default:
diff --git a/packages/taler-wallet-core/src/operations/pending.ts 
b/packages/taler-wallet-core/src/operations/pending.ts
index a6450e08f..e7e7ffcfc 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -43,8 +43,8 @@ import {
 import { AbsoluteTime } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { GetReadOnlyAccess } from "../util/query.js";
-import { TaskIdentifiers } from "../util/retries.js";
 import { GlobalIDB } from "@gnu-taler/idb-bridge";
+import { TaskIdentifiers } from "./common.js";
 
 function getPendingCommon(
   ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index fcb7d6c98..71eb58ec9 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -53,12 +53,9 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadWriteAccess } from "../util/query.js";
-import {
-  OperationAttemptResult,
-  unwrapOperationHandlerResultOrThrow,
-} from "../util/retries.js";
 import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
 import { internalCreateWithdrawalGroup } from "./withdraw.js";
+import { OperationAttemptResult } from "./common.js";
 
 const logger = new Logger("operations/recoup.ts");
 
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index c2cf13857..e573ddb44 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -84,18 +84,12 @@ import {
 } from "@gnu-taler/taler-util/http";
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import {
-  constructTaskIdentifier,
-  OperationAttemptResult,
-  OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable } from "./common.js";
+import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, 
OperationAttemptResultType } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import { selectWithdrawalDenominations } from "../util/coinSelection.js";
 import {
   isWithdrawableDenom,
   PendingTaskType,
-  WalletConfig,
 } from "../index.js";
 import {
   constructTransactionIdentifier,
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index 1a565e02f..b43fd2e8a 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -57,12 +57,7 @@ import {
   readSuccessResponseJsonOrThrow,
 } from "@gnu-taler/taler-util/http";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import {
-  constructTaskIdentifier,
-  OperationAttemptResult,
-  OperationAttemptResultType,
-} from "../util/retries.js";
-import { makeCoinAvailable } from "./common.js";
+import { constructTaskIdentifier, makeCoinAvailable, OperationAttemptResult, 
OperationAttemptResultType } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   getCandidateWithdrawalDenoms,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index b6dc2e8bd..82b7cea64 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -69,8 +69,12 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { PendingTaskType } from "../pending-types.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { constructTaskIdentifier, TaskIdentifiers } from "../util/retries.js";
-import { resetOperationTimeout, TombstoneTag } from "./common.js";
+import {
+  constructTaskIdentifier,
+  resetPendingTaskTimeout,
+  TaskIdentifiers,
+  TombstoneTag,
+} from "./common.js";
 import {
   abortDepositGroup,
   failDepositTransaction,
@@ -1388,7 +1392,7 @@ export async function retryTransaction(
         tag: PendingTaskType.PeerPullCredit,
         pursePub: parsedTx.pursePub,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1397,7 +1401,7 @@ export async function retryTransaction(
         tag: PendingTaskType.Deposit,
         depositGroupId: parsedTx.depositGroupId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1408,7 +1412,7 @@ export async function retryTransaction(
         tag: PendingTaskType.Withdraw,
         withdrawalGroupId: parsedTx.withdrawalGroupId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1417,7 +1421,7 @@ export async function retryTransaction(
         tag: PendingTaskType.Purchase,
         proposalId: parsedTx.proposalId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1426,7 +1430,7 @@ export async function retryTransaction(
         tag: PendingTaskType.TipPickup,
         walletTipId: parsedTx.walletTipId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1435,7 +1439,7 @@ export async function retryTransaction(
         tag: PendingTaskType.Refresh,
         refreshGroupId: parsedTx.refreshGroupId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1444,7 +1448,7 @@ export async function retryTransaction(
         tag: PendingTaskType.PeerPullDebit,
         peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1453,7 +1457,7 @@ export async function retryTransaction(
         tag: PendingTaskType.PeerPushCredit,
         peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
@@ -1462,7 +1466,7 @@ export async function retryTransaction(
         tag: PendingTaskType.PeerPushDebit,
         pursePub: parsedTx.pursePub,
       });
-      await resetOperationTimeout(ws, taskId);
+      await resetPendingTaskTimeout(ws, taskId);
       stopLongpolling(ws, taskId);
       break;
     }
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 88389fd99..dd07bdebc 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -92,10 +92,13 @@ import {
 } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import {
+  OperationAttemptResult,
+  OperationAttemptResultType,
+  TaskIdentifiers,
+  constructTaskIdentifier,
   makeCoinAvailable,
   makeExchangeListItem,
   runLongpollAsync,
-  runOperationWithErrorReporting,
 } from "../operations/common.js";
 import {
   HttpRequestLibrary,
@@ -114,12 +117,6 @@ import {
   GetReadOnlyAccess,
   GetReadWriteAccess,
 } from "../util/query.js";
-import {
-  OperationAttemptResult,
-  OperationAttemptResultType,
-  TaskIdentifiers,
-  constructTaskIdentifier,
-} from "../util/retries.js";
 import {
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
   WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -1225,10 +1222,6 @@ async function queryReserve(
       result.talerErrorResponse.code ===
         TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
     ) {
-      ws.notify({
-        type: NotificationType.ReserveNotYetFound,
-        reservePub,
-      });
       return { ready: false };
     } else {
       throwUnexpectedRequestError(resp, result.talerErrorResponse);
@@ -1258,12 +1251,6 @@ async function queryReserve(
 
   notifyTransition(ws, transactionId, transitionResult);
 
-  // FIXME: This notification is deprecated with DD37
-  ws.notify({
-    type: NotificationType.WithdrawalGroupReserveReady,
-    transactionId,
-  });
-
   return { ready: true };
 }
 
@@ -2053,8 +2040,6 @@ async function registerReserveWithBank(
     });
 
   notifyTransition(ws, transactionId, transitionInfo);
-  // FIXME: This notification is deprecated with DD37
-  ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
 }
 
 interface BankStatusResult {
@@ -2176,15 +2161,6 @@ async function processReserveBankStatus(
         const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
         r.wgInfo.bankInfo.timestampBankConfirmed = now;
         r.status = WithdrawalGroupStatus.PendingQueryingStatus;
-        // FIXME: Notification is deprecated with DD37.
-        const transactionId = constructTransactionIdentifier({
-          tag: TransactionType.Withdrawal,
-          withdrawalGroupId: r.withdrawalGroupId,
-        });
-        ws.notify({
-          type: NotificationType.WithdrawalGroupBankConfirmed,
-          transactionId,
-        });
       } else {
         logger.info("withdrawal: transfer not yet confirmed by bank");
         r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
diff --git a/packages/taler-wallet-core/src/pending-types.ts 
b/packages/taler-wallet-core/src/pending-types.ts
index e85f0d460..3bb6636ee 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 { TalerErrorDetail, AbsoluteTime } from "@gnu-taler/taler-util";
-import { RetryInfo } from "./util/retries.js";
+import { RetryInfo } from "./operations/common.js";
 
 export enum PendingTaskType {
   ExchangeUpdate = "exchange-update",
diff --git a/packages/taler-wallet-core/src/util/retries.ts 
b/packages/taler-wallet-core/src/util/retries.ts
index e85eb0a6b..e602d4702 100644
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ b/packages/taler-wallet-core/src/util/retries.ts
@@ -50,309 +50,3 @@ import { assertUnreachable } from "./assertUnreachable.js";
 
 const logger = new Logger("util/retries.ts");
 
-export enum OperationAttemptResultType {
-  Finished = "finished",
-  Pending = "pending",
-  Error = "error",
-  Longpoll = "longpoll",
-}
-
-export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
-  | OperationAttemptFinishedResult<TSuccess>
-  | OperationAttemptErrorResult
-  | OperationAttemptLongpollResult
-  | OperationAttemptPendingResult<TPending>;
-
-export namespace OperationAttemptResult {
-  export function finishedEmpty(): OperationAttemptResult<unknown, unknown> {
-    return {
-      type: OperationAttemptResultType.Finished,
-      result: undefined,
-    };
-  }
-  export function pendingEmpty(): OperationAttemptResult<unknown, unknown> {
-    return {
-      type: OperationAttemptResultType.Pending,
-      result: undefined,
-    };
-  }
-  export function longpoll(): OperationAttemptResult<unknown, unknown> {
-    return {
-      type: OperationAttemptResultType.Longpoll,
-    };
-  }
-}
-
-export interface OperationAttemptFinishedResult<T> {
-  type: OperationAttemptResultType.Finished;
-  result: T;
-}
-
-export interface OperationAttemptPendingResult<T> {
-  type: OperationAttemptResultType.Pending;
-  result: T;
-}
-
-export interface OperationAttemptErrorResult {
-  type: OperationAttemptResultType.Error;
-  errorDetail: TalerErrorDetail;
-}
-
-export interface OperationAttemptLongpollResult {
-  type: OperationAttemptResultType.Longpoll;
-}
-
-export interface RetryInfo {
-  firstTry: AbsoluteTime;
-  nextRetry: AbsoluteTime;
-  retryCounter: number;
-}
-
-export interface RetryPolicy {
-  readonly backoffDelta: Duration;
-  readonly backoffBase: number;
-  readonly maxTimeout: Duration;
-}
-
-const defaultRetryPolicy: RetryPolicy = {
-  backoffBase: 1.5,
-  backoffDelta: Duration.fromSpec({ seconds: 1 }),
-  maxTimeout: Duration.fromSpec({ minutes: 2 }),
-};
-
-function updateTimeout(
-  r: RetryInfo,
-  p: RetryPolicy = defaultRetryPolicy,
-): void {
-  const now = AbsoluteTime.now();
-  if (now.t_ms === "never") {
-    throw Error("assertion failed");
-  }
-  if (p.backoffDelta.d_ms === "forever") {
-    r.nextRetry = AbsoluteTime.never();
-    return;
-  }
-
-  const nextIncrement =
-    p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
-
-  const t =
-    now.t_ms +
-    (p.maxTimeout.d_ms === "forever"
-      ? nextIncrement
-      : Math.min(p.maxTimeout.d_ms, nextIncrement));
-  r.nextRetry = AbsoluteTime.fromMilliseconds(t);
-}
-
-export namespace RetryInfo {
-  export function getDuration(
-    r: RetryInfo | undefined,
-    p: RetryPolicy = defaultRetryPolicy,
-  ): Duration {
-    if (!r) {
-      // If we don't have any retry info, run immediately.
-      return { d_ms: 0 };
-    }
-    if (p.backoffDelta.d_ms === "forever") {
-      return { d_ms: "forever" };
-    }
-    const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
-    return {
-      d_ms:
-        p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
-    };
-  }
-
-  export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
-    const now = AbsoluteTime.now();
-    const info = {
-      firstTry: now,
-      nextRetry: now,
-      retryCounter: 0,
-    };
-    updateTimeout(info, p);
-    return info;
-  }
-
-  export function increment(
-    r: RetryInfo | undefined,
-    p: RetryPolicy = defaultRetryPolicy,
-  ): RetryInfo {
-    if (!r) {
-      return reset(p);
-    }
-    const r2 = { ...r };
-    r2.retryCounter++;
-    updateTimeout(r2, p);
-    return r2;
-  }
-}
-
-/**
- * Parsed representation of task identifiers.
- */
-export type ParsedTaskIdentifier =
-  | {
-      tag: PendingTaskType.Withdraw;
-      withdrawalGroupId: string;
-    }
-  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
-  | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
-  | { tag: PendingTaskType.Deposit; depositGroupId: string }
-  | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
-  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
-  | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
-  | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
-  | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
-  | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
-  | { tag: PendingTaskType.Purchase; proposalId: string }
-  | { tag: PendingTaskType.Recoup; recoupGroupId: string }
-  | { tag: PendingTaskType.TipPickup; walletTipId: string }
-  | { tag: PendingTaskType.Refresh; refreshGroupId: string };
-
-export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
-  throw Error("not yet implemented");
-}
-
-export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
-  switch (p.tag) {
-    case PendingTaskType.Backup:
-      return `${p.tag}:${p.backupProviderBaseUrl}` as TaskId;
-    case PendingTaskType.Deposit:
-      return `${p.tag}:${p.depositGroupId}` as TaskId;
-    case PendingTaskType.ExchangeCheckRefresh:
-      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
-    case PendingTaskType.ExchangeUpdate:
-      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
-    case PendingTaskType.PeerPullDebit:
-      return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId;
-    case PendingTaskType.PeerPushCredit:
-      return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId;
-    case PendingTaskType.PeerPullCredit:
-      return `${p.tag}:${p.pursePub}` as TaskId;
-    case PendingTaskType.PeerPushDebit:
-      return `${p.tag}:${p.pursePub}` as TaskId;
-    case PendingTaskType.Purchase:
-      return `${p.tag}:${p.proposalId}` as TaskId;
-    case PendingTaskType.Recoup:
-      return `${p.tag}:${p.recoupGroupId}` as TaskId;
-    case PendingTaskType.Refresh:
-      return `${p.tag}:${p.refreshGroupId}` as TaskId;
-    case PendingTaskType.TipPickup:
-      return `${p.tag}:${p.walletTipId}` as TaskId;
-    case PendingTaskType.Withdraw:
-      return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
-    default:
-      assertUnreachable(p);
-  }
-}
-
-export namespace TaskIdentifiers {
-  export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
-    return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
-  }
-  export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
-    return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
-  }
-  export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
-    return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
-  }
-  export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
-    return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
-  }
-  export function forTipPickup(tipRecord: TipRecord): TaskId {
-    return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
-  }
-  export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
-    return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` 
as TaskId;
-  }
-  export function forPay(purchaseRecord: PurchaseRecord): TaskId {
-    return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as 
TaskId;
-  }
-  export function forRecoup(recoupRecord: RecoupGroupRecord): TaskId {
-    return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskId;
-  }
-  export function forDeposit(depositRecord: DepositGroupRecord): TaskId {
-    return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as 
TaskId;
-  }
-  export function forBackup(backupRecord: BackupProviderRecord): TaskId {
-    return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
-  }
-  export function forPeerPushPaymentInitiation(
-    ppi: PeerPushPaymentInitiationRecord,
-  ): TaskId {
-    return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
-  }
-  export function forPeerPullPaymentInitiation(
-    ppi: PeerPullPaymentInitiationRecord,
-  ): TaskId {
-    return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
-  }
-  export function forPeerPullPaymentDebit(
-    ppi: PeerPullPaymentIncomingRecord,
-  ): TaskId {
-    return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` 
as TaskId;
-  }
-  export function forPeerPushCredit(
-    ppi: PeerPushPaymentIncomingRecord,
-  ): TaskId {
-    return 
`${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId;
-  }
-}
-
-export async function scheduleRetryInTx(
-  ws: InternalWalletState,
-  tx: GetReadWriteAccess<{
-    operationRetries: typeof WalletStoresV1.operationRetries;
-  }>,
-  opId: string,
-  errorDetail?: TalerErrorDetail,
-): Promise<void> {
-  let retryRecord = await tx.operationRetries.get(opId);
-  if (!retryRecord) {
-    retryRecord = {
-      id: opId,
-      retryInfo: RetryInfo.reset(),
-    };
-    if (errorDetail) {
-      retryRecord.lastError = errorDetail;
-    }
-  } else {
-    retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
-    if (errorDetail) {
-      retryRecord.lastError = errorDetail;
-    } else {
-      delete retryRecord.lastError;
-    }
-  }
-  await tx.operationRetries.put(retryRecord);
-}
-
-export async function scheduleRetry(
-  ws: InternalWalletState,
-  opId: string,
-  errorDetail?: TalerErrorDetail,
-): Promise<void> {
-  return await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      tx.operationRetries;
-      scheduleRetryInTx(ws, tx, opId, errorDetail);
-    });
-}
-
-/**
- * Run an operation handler, expect a success result and extract the success 
value.
- */
-export async function unwrapOperationHandlerResultOrThrow<T>(
-  res: OperationAttemptResult<T>,
-): Promise<T> {
-  switch (res.type) {
-    case OperationAttemptResultType.Finished:
-      return res.result;
-    case OperationAttemptResultType.Error:
-      throw TalerError.fromUncheckedDetail(res.errorDetail);
-    default:
-      throw Error(`unexpected operation result (${res.type})`);
-  }
-}
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index a04464630..e5cd713b8 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -48,6 +48,7 @@ import {
   RefreshReason,
   TalerError,
   TalerErrorCode,
+  TransactionState,
   TransactionType,
   URL,
   ValidateIbanResponse,
@@ -170,9 +171,10 @@ import { getBalanceDetail, getBalances } from 
"./operations/balance.js";
 import {
   getExchangeTosStatus,
   makeExchangeListItem,
-  runOperationWithErrorReporting,
+  runTaskWithErrorReporting,
 } from "./operations/common.js";
 import {
+  computeDepositTransactionStatus,
   createDepositGroup,
   generateDepositGroupTxId,
   prepareDepositGroup,
@@ -191,6 +193,9 @@ import {
 } from "./operations/exchanges.js";
 import { getMerchantInfo } from "./operations/merchants.js";
 import {
+  computePayMerchantTransactionActions,
+  computePayMerchantTransactionState,
+  computeRefundTransactionState,
   confirmPay,
   getContractTermsDetails,
   preparePayForUri,
@@ -200,21 +205,25 @@ import {
 } from "./operations/pay-merchant.js";
 import {
   checkPeerPullPaymentInitiation,
+  computePeerPullCreditTransactionState,
   initiatePeerPullPayment,
   processPeerPullCredit,
 } from "./operations/pay-peer-pull-credit.js";
 import {
+  computePeerPullDebitTransactionState,
   confirmPeerPullDebit,
   preparePeerPullDebit,
   processPeerPullDebit,
 } from "./operations/pay-peer-pull-debit.js";
 import {
+  computePeerPushCreditTransactionState,
   confirmPeerPushCredit,
   preparePeerPushCredit,
   processPeerPushCredit,
 } from "./operations/pay-peer-push-credit.js";
 import {
   checkPeerPushDebit,
+  computePeerPushDebitTransactionState,
   initiatePeerPushDebit,
   processPeerPushDebit,
 } from "./operations/pay-peer-push-debit.js";
@@ -222,6 +231,7 @@ import { getPendingOperations } from 
"./operations/pending.js";
 import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
 import {
   autoRefresh,
+  computeRefreshTransactionState,
   createRefreshGroup,
   processRefreshGroup,
 } from "./operations/refresh.js";
@@ -231,7 +241,7 @@ import {
   testPay,
   withdrawTestBalance,
 } from "./operations/testing.js";
-import { acceptTip, prepareTip, processTip } from "./operations/tip.js";
+import { acceptTip, computeTipTransactionStatus, prepareTip, processTip } from 
"./operations/tip.js";
 import {
   abortTransaction,
   deleteTransaction,
@@ -245,6 +255,7 @@ import {
 } from "./operations/transactions.js";
 import {
   acceptWithdrawalFromUri,
+  computeWithdrawalTransactionStatus,
   createManualWithdrawal,
   getExchangeWithdrawalInfo,
   getWithdrawalDetailsForUri,
@@ -268,7 +279,7 @@ import {
   GetReadOnlyAccess,
   GetReadWriteAccess,
 } from "./util/query.js";
-import { OperationAttemptResult, TaskIdentifiers } from "./util/retries.js";
+import { OperationAttemptResult, TaskIdentifiers } from 
"./operations/common.js";
 import { TimerAPI, TimerGroup } from "./util/timer.js";
 import {
   WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -337,7 +348,7 @@ export async function runPending(ws: InternalWalletState): 
Promise<void> {
     if (!AbsoluteTime.isExpired(p.timestampDue)) {
       continue;
     }
-    await runOperationWithErrorReporting(ws, p.id, async () => {
+    await runTaskWithErrorReporting(ws, p.id, async () => {
       logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
       return await callOperationHandler(ws, p);
     });
@@ -439,7 +450,7 @@ async function runTaskLoop(
         if (!AbsoluteTime.isExpired(p.timestampDue)) {
           continue;
         }
-        await runOperationWithErrorReporting(ws, p.id, async () => {
+        await runTaskWithErrorReporting(ws, p.id, async () => {
           logger.trace(`running pending ${JSON.stringify(p, undefined, 2)}`);
           return await callOperationHandler(ws, p);
         });
@@ -1711,6 +1722,93 @@ class InternalWalletStateImpl implements 
InternalWalletState {
     }
   }
 
+  async getTransactionState(
+    ws: InternalWalletState,
+    tx: GetReadOnlyAccess<typeof WalletStoresV1>,
+    transactionId: string,
+  ): Promise<TransactionState | undefined> {
+    const parsedTxId = parseTransactionIdentifier(transactionId);
+    if (!parsedTxId) {
+      throw Error("invalid tx identifier");
+    }
+    switch (parsedTxId.tag) {
+      case TransactionType.Deposit: {
+        const rec = await tx.depositGroups.get(parsedTxId.depositGroupId);
+        if (!rec) {
+          return undefined;
+        }
+        return computeDepositTransactionStatus(rec);
+      }
+      case TransactionType.InternalWithdrawal:
+      case TransactionType.Withdrawal: {
+        const rec = await 
tx.withdrawalGroups.get(parsedTxId.withdrawalGroupId);
+        if (!rec) {
+          return undefined;
+        }
+        return computeWithdrawalTransactionStatus(rec);
+      }
+      case TransactionType.Payment: {
+        const rec = await tx.purchases.get(parsedTxId.proposalId);
+        if (!rec) {
+          return;
+        }
+        return computePayMerchantTransactionState(rec);
+      }
+      case TransactionType.Refund: {
+        const rec = await tx.refundGroups.get(
+          parsedTxId.refundGroupId,
+        );
+        if (!rec) {
+          return undefined;
+        }
+        return computeRefundTransactionState(rec);
+      }
+      case TransactionType.PeerPullCredit:
+        const rec = await 
tx.peerPullPaymentInitiations.get(parsedTxId.pursePub);
+        if (!rec) {
+          return undefined;
+        }
+        return computePeerPullCreditTransactionState(rec);
+      case TransactionType.PeerPullDebit: {
+        const rec = await 
tx.peerPullPaymentIncoming.get(parsedTxId.peerPullPaymentIncomingId);
+        if (!rec) {
+          return undefined;
+        }
+        return computePeerPullDebitTransactionState(rec);
+      }
+      case TransactionType.PeerPushCredit: {
+        const rec = await 
tx.peerPushPaymentIncoming.get(parsedTxId.peerPushPaymentIncomingId);
+        if (!rec) {
+          return undefined;
+        }
+        return computePeerPushCreditTransactionState(rec);
+      }
+      case TransactionType.PeerPushDebit: {
+        const rec = await 
tx.peerPushPaymentInitiations.get(parsedTxId.pursePub);
+        if (!rec) {
+          return undefined;
+        }
+        return computePeerPushDebitTransactionState(rec);
+      }
+      case TransactionType.Refresh: {
+        const rec = await tx.refreshGroups.get(parsedTxId.refreshGroupId);
+        if (!rec) {
+          return undefined;
+        }
+        return computeRefreshTransactionState(rec)
+      }
+      case TransactionType.Tip: {
+        const rec = await tx.tips.get(parsedTxId.walletTipId);
+        if (!rec) {
+          return undefined;
+        }
+        return computeTipTransactionStatus(rec);
+      }
+      default:
+        assertUnreachable(parsedTxId);
+    }
+  }
+
   async getDenomInfo(
     ws: InternalWalletState,
     tx: GetReadWriteAccess<{

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