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: uniform transact


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: uniform transaction interface, cleanup
Date: Mon, 15 Jan 2024 18:43:27 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 8da08fe42 wallet-core: uniform transaction interface, cleanup
8da08fe42 is described below

commit 8da08fe4205c1e03eec3d4925c598be0b6769ba5
Author: Florian Dold <florian@dold.me>
AuthorDate: Mon Jan 15 17:36:50 2024 +0100

    wallet-core: uniform transaction interface, cleanup
---
 .../src/integrationtests/test-peer-to-peer-pull.ts |   5 +
 packages/taler-wallet-core/src/db.ts               |   1 +
 .../taler-wallet-core/src/internal-wallet-state.ts |  11 -
 .../taler-wallet-core/src/operations/common.ts     |  10 +-
 .../taler-wallet-core/src/operations/deposits.ts   | 404 ++++++++--------
 .../src/operations/pay-merchant.ts                 | 422 +++++++++--------
 .../src/operations/pay-peer-pull-credit.ts         | 523 +++++++++++----------
 .../src/operations/pay-peer-pull-debit.ts          | 129 ++---
 .../src/operations/pay-peer-push-credit.ts         | 458 ++++++++++--------
 .../src/operations/pay-peer-push-debit.ts          | 522 ++++++++++----------
 .../taler-wallet-core/src/operations/refresh.ts    | 287 +++++------
 .../taler-wallet-core/src/operations/reward.ts     | 387 ++++++++-------
 .../src/operations/transactions.ts                 | 416 ++--------------
 .../taler-wallet-core/src/operations/withdraw.ts   | 482 ++++++++++---------
 packages/taler-wallet-core/src/util/query.ts       |   6 +-
 packages/taler-wallet-core/src/wallet.ts           |   8 +-
 16 files changed, 1944 insertions(+), 2127 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts 
b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
index a71175407..e8d34e288 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -261,8 +261,13 @@ async function checkAbortedPeerPull(
     peerPullDebitId: checkResp.peerPullDebitId,
   });
 
+  console.log(`waiting for ${resp.transactionId} to go to state aborted`);
+  console.log("checkpoint: before-aborted-wait");
   await peerPullCreditAbortedCond;
+  console.log("checkpoint: after-credit-aborted-wait");
   await peerPullDebitAbortedCond;
+  console.log("checkpoint: after-debit-aborted-wait");
+  console.log("checkpoint: after-aborted-wait");
 
   const txn1 = await wallet1.client.call(
     WalletApiOperation.GetTransactions,
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 263de9d4c..84066aaf0 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -957,6 +957,7 @@ export enum RewardRecordStatus {
   DialogAccept = 0x0101_0000,
   Done = 0x0500_0000,
   Aborted = 0x0500_0000,
+  Failed = 0x0501_000,
 }
 
 export enum RefreshCoinStatus {
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts 
b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 94f2367e1..8c49f8e5e 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -69,16 +69,6 @@ export interface MerchantInfo {
   protocolVersionCurrent: number;
 }
 
-/**
- * Interface for merchant-related operations.
- */
-export interface MerchantOperations {
-  getMerchantInfo(
-    ws: InternalWalletState,
-    merchantBaseUrl: string,
-  ): Promise<MerchantInfo>;
-}
-
 export interface RefreshOperations {
   createRefreshGroup(
     ws: InternalWalletState,
@@ -154,7 +144,6 @@ export interface InternalWalletState {
   merchantInfoCache: Record<string, MerchantInfo>;
 
   recoupOps: RecoupOperations;
-  merchantOps: MerchantOperations;
   refreshOps: RefreshOperations;
 
   isTaskLoopRunning: boolean;
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index 1103b7255..f34190cef 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -76,10 +76,7 @@ import { PendingTaskType, TaskId } from 
"../pending-types.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
-import {
-  constructTransactionIdentifier,
-  parseTransactionIdentifier,
-} from "./transactions.js";
+import { constructTransactionIdentifier } from "./transactions.js";
 
 const logger = new Logger("operations/common.ts");
 
@@ -1086,11 +1083,12 @@ export enum TransitionResult {
 
 /**
  * Transaction context.
- *
- * FIXME: Should eventually be implemented by all transactions.
+ * Uniform interface to all transactions.
  */
 export interface TransactionContext {
   abortTransaction(): Promise<void>;
+  suspendTransaction(): Promise<void>;
   resumeTransaction(): Promise<void>;
   failTransaction(): Promise<void>;
+  deleteTransaction(): Promise<void>;
 }
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index f158d9cf9..62c1e406c 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -14,6 +14,10 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+/**
+ * Implementation of the deposit transaction.
+ */
+
 /**
  * Imports.
  */
@@ -84,6 +88,7 @@ import { checkDbInvariant, checkLogicInvariant } from 
"../util/invariants.js";
 import {
   TaskRunResult,
   TombstoneTag,
+  TransactionContext,
   constructTaskIdentifier,
   runLongpollAsync,
   spendCoins,
@@ -106,6 +111,194 @@ import {
  */
 const logger = new Logger("deposits.ts");
 
+export class DepositTransactionContext implements TransactionContext {
+  private transactionId: string;
+  private retryTag: string;
+  constructor(
+    public ws: InternalWalletState,
+    public depositGroupId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Deposit,
+      depositGroupId,
+    });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.Deposit,
+      depositGroupId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const depositGroupId = this.depositGroupId;
+    const ws = this.ws;
+    // FIXME: We should check first if we are in a final state
+    // where deletion is allowed.
+    await ws.db
+      .mktx((x) => [x.depositGroups, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const tipRecord = await tx.depositGroups.get(depositGroupId);
+        if (tipRecord) {
+          await tx.depositGroups.delete(depositGroupId);
+          await tx.tombstones.put({
+            id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
+          });
+        }
+      });
+    return;
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, depositGroupId, transactionId, retryTag } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.depositGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.depositGroups.get(depositGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't suspend deposit group, depositGroupId=${depositGroupId} not 
found`,
+          );
+          return undefined;
+        }
+        const oldState = computeDepositTransactionStatus(dg);
+        let newOpStatus: DepositOperationStatus | undefined;
+        switch (dg.operationStatus) {
+          case DepositOperationStatus.PendingDeposit:
+            newOpStatus = DepositOperationStatus.SuspendedDeposit;
+            break;
+          case DepositOperationStatus.PendingKyc:
+            newOpStatus = DepositOperationStatus.SuspendedKyc;
+            break;
+          case DepositOperationStatus.PendingTrack:
+            newOpStatus = DepositOperationStatus.SuspendedTrack;
+            break;
+          case DepositOperationStatus.Aborting:
+            newOpStatus = DepositOperationStatus.SuspendedAborting;
+            break;
+        }
+        if (!newOpStatus) {
+          return undefined;
+        }
+        dg.operationStatus = newOpStatus;
+        await tx.depositGroups.put(dg);
+        return {
+          oldTxState: oldState,
+          newTxState: computeDepositTransactionStatus(dg),
+        };
+      });
+    stopLongpolling(ws, retryTag);
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, depositGroupId, transactionId, retryTag } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.depositGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.depositGroups.get(depositGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't suspend deposit group, depositGroupId=${depositGroupId} not 
found`,
+          );
+          return undefined;
+        }
+        const oldState = computeDepositTransactionStatus(dg);
+        switch (dg.operationStatus) {
+          case DepositOperationStatus.Finished:
+            return undefined;
+          case DepositOperationStatus.PendingDeposit: {
+            dg.operationStatus = DepositOperationStatus.Aborting;
+            await tx.depositGroups.put(dg);
+            return {
+              oldTxState: oldState,
+              newTxState: computeDepositTransactionStatus(dg),
+            };
+          }
+          case DepositOperationStatus.SuspendedDeposit:
+            // FIXME: Can we abort a suspended transaction?!
+            return undefined;
+        }
+        return undefined;
+      });
+    stopLongpolling(ws, retryTag);
+    // Need to process the operation again.
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, depositGroupId, transactionId, retryTag } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.depositGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.depositGroups.get(depositGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't resume deposit group, depositGroupId=${depositGroupId} not 
found`,
+          );
+          return;
+        }
+        const oldState = computeDepositTransactionStatus(dg);
+        let newOpStatus: DepositOperationStatus | undefined;
+        switch (dg.operationStatus) {
+          case DepositOperationStatus.SuspendedDeposit:
+            newOpStatus = DepositOperationStatus.PendingDeposit;
+            break;
+          case DepositOperationStatus.SuspendedAborting:
+            newOpStatus = DepositOperationStatus.Aborting;
+            break;
+          case DepositOperationStatus.SuspendedKyc:
+            newOpStatus = DepositOperationStatus.PendingKyc;
+            break;
+          case DepositOperationStatus.SuspendedTrack:
+            newOpStatus = DepositOperationStatus.PendingTrack;
+            break;
+        }
+        if (!newOpStatus) {
+          return undefined;
+        }
+        dg.operationStatus = newOpStatus;
+        await tx.depositGroups.put(dg);
+        return {
+          oldTxState: oldState,
+          newTxState: computeDepositTransactionStatus(dg),
+        };
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, depositGroupId, transactionId, retryTag } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.depositGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.depositGroups.get(depositGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't cancel aborting deposit group, 
depositGroupId=${depositGroupId} not found`,
+          );
+          return undefined;
+        }
+        const oldState = computeDepositTransactionStatus(dg);
+        switch (dg.operationStatus) {
+          case DepositOperationStatus.SuspendedAborting:
+          case DepositOperationStatus.Aborting: {
+            dg.operationStatus = DepositOperationStatus.Failed;
+            await tx.depositGroups.put(dg);
+            return {
+              oldTxState: oldState,
+              newTxState: computeDepositTransactionStatus(dg),
+            };
+          }
+        }
+        return undefined;
+      });
+    // FIXME: Also cancel ongoing work (via cancellation token, once 
implemented)
+    stopLongpolling(ws, retryTag);
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 /**
  * Get the (DD37-style) transaction status based on the
  * database record of a deposit group.
@@ -203,217 +396,6 @@ export function computeDepositTransactionActions(
   }
 }
 
-/**
- * Put a deposit group in a suspended state.
- * While the deposit group is suspended, no network requests
- * will be made to advance the transaction status.
- */
-export async function suspendDepositGroup(
-  ws: InternalWalletState,
-  depositGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Deposit,
-    depositGroupId,
-  });
-  const retryTag = constructTaskIdentifier({
-    tag: PendingTaskType.Deposit,
-    depositGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.depositGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.depositGroups.get(depositGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't suspend deposit group, depositGroupId=${depositGroupId} not 
found`,
-        );
-        return undefined;
-      }
-      const oldState = computeDepositTransactionStatus(dg);
-      let newOpStatus: DepositOperationStatus | undefined;
-      switch (dg.operationStatus) {
-        case DepositOperationStatus.PendingDeposit:
-          newOpStatus = DepositOperationStatus.SuspendedDeposit;
-          break;
-        case DepositOperationStatus.PendingKyc:
-          newOpStatus = DepositOperationStatus.SuspendedKyc;
-          break;
-        case DepositOperationStatus.PendingTrack:
-          newOpStatus = DepositOperationStatus.SuspendedTrack;
-          break;
-        case DepositOperationStatus.Aborting:
-          newOpStatus = DepositOperationStatus.SuspendedAborting;
-          break;
-      }
-      if (!newOpStatus) {
-        return undefined;
-      }
-      dg.operationStatus = newOpStatus;
-      await tx.depositGroups.put(dg);
-      return {
-        oldTxState: oldState,
-        newTxState: computeDepositTransactionStatus(dg),
-      };
-    });
-  stopLongpolling(ws, retryTag);
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeDepositGroup(
-  ws: InternalWalletState,
-  depositGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Deposit,
-    depositGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.depositGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.depositGroups.get(depositGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't resume deposit group, depositGroupId=${depositGroupId} not 
found`,
-        );
-        return;
-      }
-      const oldState = computeDepositTransactionStatus(dg);
-      let newOpStatus: DepositOperationStatus | undefined;
-      switch (dg.operationStatus) {
-        case DepositOperationStatus.SuspendedDeposit:
-          newOpStatus = DepositOperationStatus.PendingDeposit;
-          break;
-        case DepositOperationStatus.SuspendedAborting:
-          newOpStatus = DepositOperationStatus.Aborting;
-          break;
-        case DepositOperationStatus.SuspendedKyc:
-          newOpStatus = DepositOperationStatus.PendingKyc;
-          break;
-        case DepositOperationStatus.SuspendedTrack:
-          newOpStatus = DepositOperationStatus.PendingTrack;
-          break;
-      }
-      if (!newOpStatus) {
-        return undefined;
-      }
-      dg.operationStatus = newOpStatus;
-      await tx.depositGroups.put(dg);
-      return {
-        oldTxState: oldState,
-        newTxState: computeDepositTransactionStatus(dg),
-      };
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function abortDepositGroup(
-  ws: InternalWalletState,
-  depositGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Deposit,
-    depositGroupId,
-  });
-  const retryTag = constructTaskIdentifier({
-    tag: PendingTaskType.Deposit,
-    depositGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.depositGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.depositGroups.get(depositGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't suspend deposit group, depositGroupId=${depositGroupId} not 
found`,
-        );
-        return undefined;
-      }
-      const oldState = computeDepositTransactionStatus(dg);
-      switch (dg.operationStatus) {
-        case DepositOperationStatus.Finished:
-          return undefined;
-        case DepositOperationStatus.PendingDeposit: {
-          dg.operationStatus = DepositOperationStatus.Aborting;
-          await tx.depositGroups.put(dg);
-          return {
-            oldTxState: oldState,
-            newTxState: computeDepositTransactionStatus(dg),
-          };
-        }
-        case DepositOperationStatus.SuspendedDeposit:
-          // FIXME: Can we abort a suspended transaction?!
-          return undefined;
-      }
-      return undefined;
-    });
-  stopLongpolling(ws, retryTag);
-  // Need to process the operation again.
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failDepositTransaction(
-  ws: InternalWalletState,
-  depositGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Deposit,
-    depositGroupId,
-  });
-  const retryTag = constructTaskIdentifier({
-    tag: PendingTaskType.Deposit,
-    depositGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.depositGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.depositGroups.get(depositGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't cancel aborting deposit group, 
depositGroupId=${depositGroupId} not found`,
-        );
-        return undefined;
-      }
-      const oldState = computeDepositTransactionStatus(dg);
-      switch (dg.operationStatus) {
-        case DepositOperationStatus.SuspendedAborting:
-        case DepositOperationStatus.Aborting: {
-          dg.operationStatus = DepositOperationStatus.Failed;
-          await tx.depositGroups.put(dg);
-          return {
-            oldTxState: oldState,
-            newTxState: computeDepositTransactionStatus(dg),
-          };
-        }
-      }
-      return undefined;
-    });
-  // FIXME: Also cancel ongoing work (via cancellation token, once implemented)
-  stopLongpolling(ws, retryTag);
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function deleteDepositGroup(
-  ws: InternalWalletState,
-  depositGroupId: string,
-) {
-  // FIXME: We should check first if we are in a final state
-  // where deletion is allowed.
-  await ws.db
-    .mktx((x) => [x.depositGroups, x.tombstones])
-    .runReadWrite(async (tx) => {
-      const tipRecord = await tx.depositGroups.get(depositGroupId);
-      if (tipRecord) {
-        await tx.depositGroups.delete(depositGroupId);
-        await tx.tombstones.put({
-          id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId,
-        });
-      }
-    });
-}
-
 /**
  * Check whether the refresh associated with the
  * aborting deposit group is done.
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts 
b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index a81311702..bc9e94a21 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -94,7 +94,6 @@ import {
 } from "@gnu-taler/taler-util/http";
 import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
 import {
-  BackupProviderStateTag,
   CoinRecord,
   DenominationRecord,
   PurchaseRecord,
@@ -130,6 +129,8 @@ import {
   TaskIdentifiers,
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
 } from "./common.js";
 import {
   calculateRefreshOutput,
@@ -147,6 +148,224 @@ import {
  */
 const logger = new Logger("pay-merchant.ts");
 
+export class PayMerchantTransactionContext implements TransactionContext {
+  private transactionId: string;
+  private retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public proposalId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Payment,
+      proposalId,
+    });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.Purchase,
+      proposalId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, proposalId } = this;
+    await ws.db
+      .mktx((x) => [x.purchases, x.tombstones])
+      .runReadWrite(async (tx) => {
+        let found = false;
+        const purchase = await tx.purchases.get(proposalId);
+        if (purchase) {
+          found = true;
+          await tx.purchases.delete(proposalId);
+        }
+        if (found) {
+          await tx.tombstones.put({
+            id: TombstoneTag.DeletePayment + ":" + proposalId,
+          });
+        }
+      });
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, proposalId, transactionId } = this;
+    stopLongpolling(ws, this.retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.purchases])
+      .runReadWrite(async (tx) => {
+        const purchase = await tx.purchases.get(proposalId);
+        if (!purchase) {
+          throw Error("purchase not found");
+        }
+        const oldTxState = computePayMerchantTransactionState(purchase);
+        let newStatus = transitionSuspend[purchase.purchaseStatus];
+        if (!newStatus) {
+          return undefined;
+        }
+        await tx.purchases.put(purchase);
+        const newTxState = computePayMerchantTransactionState(purchase);
+        return { oldTxState, newTxState };
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+    ws.workAvailable.trigger();
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, proposalId, transactionId } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [
+        x.purchases,
+        x.refreshGroups,
+        x.denominations,
+        x.coinAvailability,
+        x.coins,
+        x.operationRetries,
+      ])
+      .runReadWrite(async (tx) => {
+        const purchase = await tx.purchases.get(proposalId);
+        if (!purchase) {
+          throw Error("purchase not found");
+        }
+        const oldTxState = computePayMerchantTransactionState(purchase);
+        const oldStatus = purchase.purchaseStatus;
+        if (purchase.timestampFirstSuccessfulPay) {
+          // No point in aborting it.  We don't even report an error.
+          logger.warn(`tried to abort successful payment`);
+          return;
+        }
+        if (oldStatus === PurchaseStatus.PendingPaying) {
+          purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
+        }
+        await tx.purchases.put(purchase);
+        if (oldStatus === PurchaseStatus.PendingPaying) {
+          if (purchase.payInfo) {
+            const coinSel = purchase.payInfo.payCoinSelection;
+            const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost);
+            const refreshCoins: CoinRefreshRequest[] = [];
+            for (let i = 0; i < coinSel.coinPubs.length; i++) {
+              refreshCoins.push({
+                amount: coinSel.coinContributions[i],
+                coinPub: coinSel.coinPubs[i],
+              });
+            }
+            await createRefreshGroup(
+              ws,
+              tx,
+              currency,
+              refreshCoins,
+              RefreshReason.AbortPay,
+            );
+          }
+        }
+        await tx.operationRetries.delete(this.retryTag);
+        const newTxState = computePayMerchantTransactionState(purchase);
+        return { oldTxState, newTxState };
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+    ws.workAvailable.trigger();
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, proposalId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.purchases])
+      .runReadWrite(async (tx) => {
+        const purchase = await tx.purchases.get(proposalId);
+        if (!purchase) {
+          throw Error("purchase not found");
+        }
+        const oldTxState = computePayMerchantTransactionState(purchase);
+        let newStatus = transitionResume[purchase.purchaseStatus];
+        if (!newStatus) {
+          return undefined;
+        }
+        await tx.purchases.put(purchase);
+        const newTxState = computePayMerchantTransactionState(purchase);
+        return { oldTxState, newTxState };
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+    ws.workAvailable.trigger();
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, proposalId, transactionId } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [
+        x.purchases,
+        x.refreshGroups,
+        x.denominations,
+        x.coinAvailability,
+        x.coins,
+        x.operationRetries,
+      ])
+      .runReadWrite(async (tx) => {
+        const purchase = await tx.purchases.get(proposalId);
+        if (!purchase) {
+          throw Error("purchase not found");
+        }
+        const oldTxState = computePayMerchantTransactionState(purchase);
+        let newState: PurchaseStatus | undefined = undefined;
+        switch (purchase.purchaseStatus) {
+          case PurchaseStatus.AbortingWithRefund:
+            newState = PurchaseStatus.FailedAbort;
+            break;
+        }
+        if (newState) {
+          purchase.purchaseStatus = newState;
+          await tx.purchases.put(purchase);
+        }
+        const newTxState = computePayMerchantTransactionState(purchase);
+        return { oldTxState, newTxState };
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+    ws.workAvailable.trigger();
+  }
+}
+
+export class RefundTransactionContext implements TransactionContext {
+  public transactionId: string;
+  constructor(
+    public ws: InternalWalletState,
+    public refundGroupId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Refund,
+      refundGroupId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, refundGroupId, transactionId } = this;
+    await ws.db
+      .mktx((x) => [x.refundGroups, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const refundRecord = await tx.refundGroups.get(refundGroupId);
+        if (!refundRecord) {
+          return;
+        }
+        await tx.refundGroups.delete(refundGroupId);
+        await tx.tombstones.put({ id: transactionId });
+        // FIXME: Also tombstone the refund items, so that they won't reappear.
+      });
+  }
+
+  suspendTransaction(): Promise<void> {
+    throw new Error("Unsupported operation");
+  }
+
+  abortTransaction(): Promise<void> {
+    throw new Error("Unsupported operation");
+  }
+
+  resumeTransaction(): Promise<void> {
+    throw new Error("Unsupported operation");
+  }
+
+  failTransaction(): Promise<void> {
+    throw new Error("Unsupported operation");
+  }
+}
+
 /**
  * Compute the total cost of a payment to the customer.
  *
@@ -949,27 +1168,6 @@ async function handleInsufficientFunds(
   });
 }
 
-async function unblockBackup(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.backupProviders])
-    .runReadWrite(async (tx) => {
-      await tx.backupProviders.indexes.byPaymentProposalId
-        .iter(proposalId)
-        .forEachAsync(async (bp) => {
-          bp.state = {
-            tag: BackupProviderStateTag.Ready,
-            nextBackupTimestamp: timestampPreciseToDb(
-              TalerPreciseTimestamp.now(),
-            ),
-          };
-          tx.backupProviders.put(bp);
-        });
-    });
-}
-
 // FIXME: Should probably not be exported in its current state
 // FIXME: Should take a transaction ID instead of a proposal ID
 // FIXME: Does way more than checking the payment
@@ -1606,7 +1804,7 @@ export async function processPurchase(
   }
 }
 
-export async function processPurchasePay(
+async function processPurchasePay(
   ws: InternalWalletState,
   proposalId: string,
   options: unknown = {},
@@ -1772,7 +1970,6 @@ export async function processPurchasePay(
     }
 
     await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp);
-    await unblockBackup(ws, proposalId);
   } else {
     const payAgainUrl = new URL(
       `orders/${download.contractData.orderId}/paid`,
@@ -1799,7 +1996,6 @@ export async function processPurchasePay(
       );
     }
     await storePayReplaySuccess(ws, proposalId, sessionId);
-    await unblockBackup(ws, proposalId);
   }
 
   return TaskRunResult.finished();
@@ -1837,115 +2033,6 @@ export async function refuseProposal(
   notifyTransition(ws, transactionId, transitionInfo);
 }
 
-export async function abortPayMerchant(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Payment,
-    proposalId,
-  });
-  const opId = constructTaskIdentifier({
-    tag: PendingTaskType.Purchase,
-    proposalId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [
-      x.purchases,
-      x.refreshGroups,
-      x.denominations,
-      x.coinAvailability,
-      x.coins,
-      x.operationRetries,
-    ])
-    .runReadWrite(async (tx) => {
-      const purchase = await tx.purchases.get(proposalId);
-      if (!purchase) {
-        throw Error("purchase not found");
-      }
-      const oldTxState = computePayMerchantTransactionState(purchase);
-      const oldStatus = purchase.purchaseStatus;
-      if (purchase.timestampFirstSuccessfulPay) {
-        // No point in aborting it.  We don't even report an error.
-        logger.warn(`tried to abort successful payment`);
-        return;
-      }
-      if (oldStatus === PurchaseStatus.PendingPaying) {
-        purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
-      }
-      await tx.purchases.put(purchase);
-      if (oldStatus === PurchaseStatus.PendingPaying) {
-        if (purchase.payInfo) {
-          const coinSel = purchase.payInfo.payCoinSelection;
-          const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost);
-          const refreshCoins: CoinRefreshRequest[] = [];
-          for (let i = 0; i < coinSel.coinPubs.length; i++) {
-            refreshCoins.push({
-              amount: coinSel.coinContributions[i],
-              coinPub: coinSel.coinPubs[i],
-            });
-          }
-          await createRefreshGroup(
-            ws,
-            tx,
-            currency,
-            refreshCoins,
-            RefreshReason.AbortPay,
-          );
-        }
-      }
-      await tx.operationRetries.delete(opId);
-      const newTxState = computePayMerchantTransactionState(purchase);
-      return { oldTxState, newTxState };
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-  ws.workAvailable.trigger();
-}
-
-export async function failPaymentTransaction(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Payment,
-    proposalId,
-  });
-  const opId = constructTaskIdentifier({
-    tag: PendingTaskType.Purchase,
-    proposalId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [
-      x.purchases,
-      x.refreshGroups,
-      x.denominations,
-      x.coinAvailability,
-      x.coins,
-      x.operationRetries,
-    ])
-    .runReadWrite(async (tx) => {
-      const purchase = await tx.purchases.get(proposalId);
-      if (!purchase) {
-        throw Error("purchase not found");
-      }
-      const oldTxState = computePayMerchantTransactionState(purchase);
-      let newState: PurchaseStatus | undefined = undefined;
-      switch (purchase.purchaseStatus) {
-        case PurchaseStatus.AbortingWithRefund:
-          newState = PurchaseStatus.FailedAbort;
-          break;
-      }
-      if (newState) {
-        purchase.purchaseStatus = newState;
-        await tx.purchases.put(purchase);
-      }
-      const newTxState = computePayMerchantTransactionState(purchase);
-      return { oldTxState, newTxState };
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-  ws.workAvailable.trigger();
-}
-
 const transitionSuspend: {
   [x in PurchaseStatus]?: {
     next: PurchaseStatus | undefined;
@@ -1990,73 +2077,6 @@ const transitionResume: {
   },
 };
 
-export async function suspendPayMerchant(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Payment,
-    proposalId,
-  });
-  const opId = constructTaskIdentifier({
-    tag: PendingTaskType.Purchase,
-    proposalId,
-  });
-  stopLongpolling(ws, opId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadWrite(async (tx) => {
-      const purchase = await tx.purchases.get(proposalId);
-      if (!purchase) {
-        throw Error("purchase not found");
-      }
-      const oldTxState = computePayMerchantTransactionState(purchase);
-      let newStatus = transitionSuspend[purchase.purchaseStatus];
-      if (!newStatus) {
-        return undefined;
-      }
-      await tx.purchases.put(purchase);
-      const newTxState = computePayMerchantTransactionState(purchase);
-      return { oldTxState, newTxState };
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-  ws.workAvailable.trigger();
-}
-
-export async function resumePayMerchant(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Payment,
-    proposalId,
-  });
-  const opId = constructTaskIdentifier({
-    tag: PendingTaskType.Purchase,
-    proposalId,
-  });
-  stopLongpolling(ws, opId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadWrite(async (tx) => {
-      const purchase = await tx.purchases.get(proposalId);
-      if (!purchase) {
-        throw Error("purchase not found");
-      }
-      const oldTxState = computePayMerchantTransactionState(purchase);
-      let newStatus = transitionResume[purchase.purchaseStatus];
-      if (!newStatus) {
-        return undefined;
-      }
-      await tx.purchases.put(purchase);
-      const newTxState = computePayMerchantTransactionState(purchase);
-      return { oldTxState, newTxState };
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-  ws.workAvailable.trigger();
-}
-
 export function computePayMerchantTransactionState(
   purchaseRecord: PurchaseRecord,
 ): TransactionState {
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 a90eceed7..e655eba4b 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
@@ -69,6 +69,8 @@ import {
   LongpollResult,
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
   constructTaskIdentifier,
   runLongpollAsync,
 } from "./common.js";
@@ -88,6 +90,275 @@ import {
 
 const logger = new Logger("pay-peer-pull-credit.ts");
 
+export class PeerPullCreditTransactionContext implements TransactionContext {
+  private transactionId: string;
+  private retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public pursePub: string,
+  ) {
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.PeerPullCredit,
+      pursePub,
+    });
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.PeerPullCredit,
+      pursePub,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, pursePub } = this;
+    await ws.db
+      .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const pullIni = await tx.peerPullCredit.get(pursePub);
+        if (!pullIni) {
+          return;
+        }
+        if (pullIni.withdrawalGroupId) {
+          const withdrawalGroupId = pullIni.withdrawalGroupId;
+          const withdrawalGroupRecord =
+            await tx.withdrawalGroups.get(withdrawalGroupId);
+          if (withdrawalGroupRecord) {
+            await tx.withdrawalGroups.delete(withdrawalGroupId);
+            await tx.tombstones.put({
+              id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+            });
+          }
+        }
+        await tx.peerPullCredit.delete(pursePub);
+        await tx.tombstones.put({
+          id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
+        });
+      });
+
+    return;
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, pursePub, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPullCredit])
+      .runReadWrite(async (tx) => {
+        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+        if (!pullCreditRec) {
+          logger.warn(`peer pull credit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+        switch (pullCreditRec.status) {
+          case PeerPullPaymentCreditStatus.PendingCreatePurse:
+            newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
+            break;
+          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+            newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
+            break;
+          case PeerPullPaymentCreditStatus.PendingWithdrawing:
+            newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
+            break;
+          case PeerPullPaymentCreditStatus.PendingReady:
+            newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
+            break;
+          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+            newStatus =
+              PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
+            break;
+          case PeerPullPaymentCreditStatus.Done:
+          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+          case PeerPullPaymentCreditStatus.SuspendedReady:
+          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+          case PeerPullPaymentCreditStatus.Aborted:
+          case PeerPullPaymentCreditStatus.Failed:
+          case PeerPullPaymentCreditStatus.Expired:
+          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+            break;
+          default:
+            assertUnreachable(pullCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          pullCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          await tx.peerPullCredit.put(pullCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, pursePub, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPullCredit])
+      .runReadWrite(async (tx) => {
+        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+        if (!pullCreditRec) {
+          logger.warn(`peer pull credit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+        switch (pullCreditRec.status) {
+          case PeerPullPaymentCreditStatus.PendingCreatePurse:
+          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+          case PeerPullPaymentCreditStatus.PendingWithdrawing:
+          case PeerPullPaymentCreditStatus.PendingReady:
+          case PeerPullPaymentCreditStatus.Done:
+          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+          case PeerPullPaymentCreditStatus.SuspendedReady:
+          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+          case PeerPullPaymentCreditStatus.Aborted:
+          case PeerPullPaymentCreditStatus.Failed:
+          case PeerPullPaymentCreditStatus.Expired:
+            break;
+          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+            newStatus = PeerPullPaymentCreditStatus.Failed;
+            break;
+          default:
+            assertUnreachable(pullCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          pullCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          await tx.peerPullCredit.put(pullCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, pursePub, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPullCredit])
+      .runReadWrite(async (tx) => {
+        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+        if (!pullCreditRec) {
+          logger.warn(`peer pull credit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+        switch (pullCreditRec.status) {
+          case PeerPullPaymentCreditStatus.PendingCreatePurse:
+          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+          case PeerPullPaymentCreditStatus.PendingWithdrawing:
+          case PeerPullPaymentCreditStatus.PendingReady:
+          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+          case PeerPullPaymentCreditStatus.Done:
+          case PeerPullPaymentCreditStatus.Failed:
+          case PeerPullPaymentCreditStatus.Expired:
+          case PeerPullPaymentCreditStatus.Aborted:
+            break;
+          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+            newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
+            break;
+          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+            newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
+            break;
+          case PeerPullPaymentCreditStatus.SuspendedReady:
+            newStatus = PeerPullPaymentCreditStatus.PendingReady;
+            break;
+          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+            newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
+            break;
+          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+            break;
+          default:
+            assertUnreachable(pullCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          pullCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          await tx.peerPullCredit.put(pullCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, pursePub, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPullCredit])
+      .runReadWrite(async (tx) => {
+        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
+        if (!pullCreditRec) {
+          logger.warn(`peer pull credit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
+        switch (pullCreditRec.status) {
+          case PeerPullPaymentCreditStatus.PendingCreatePurse:
+          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+            break;
+          case PeerPullPaymentCreditStatus.PendingWithdrawing:
+            throw Error("can't abort anymore");
+          case PeerPullPaymentCreditStatus.PendingReady:
+            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
+            break;
+          case PeerPullPaymentCreditStatus.Done:
+          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
+          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+          case PeerPullPaymentCreditStatus.SuspendedReady:
+          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
+          case PeerPullPaymentCreditStatus.Aborted:
+          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
+          case PeerPullPaymentCreditStatus.Failed:
+          case PeerPullPaymentCreditStatus.Expired:
+          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
+            break;
+          default:
+            assertUnreachable(pullCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          pullCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPullCreditTransactionState(pullCreditRec);
+          await tx.peerPullCredit.put(pullCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 async function queryPurseForPeerPullCredit(
   ws: InternalWalletState,
   pullIni: PeerPullCreditRecord,
@@ -849,258 +1120,6 @@ export async function initiatePeerPullPayment(
   };
 }
 
-export async function suspendPeerPullCreditTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullCredit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullCredit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullCredit])
-    .runReadWrite(async (tx) => {
-      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
-      if (!pullCreditRec) {
-        logger.warn(`peer pull credit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
-      switch (pullCreditRec.status) {
-        case PeerPullPaymentCreditStatus.PendingCreatePurse:
-          newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
-          break;
-        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
-          newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
-          break;
-        case PeerPullPaymentCreditStatus.PendingWithdrawing:
-          newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
-          break;
-        case PeerPullPaymentCreditStatus.PendingReady:
-          newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
-          break;
-        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
-          newStatus = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
-          break;
-        case PeerPullPaymentCreditStatus.Done:
-        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
-        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
-        case PeerPullPaymentCreditStatus.SuspendedReady:
-        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
-        case PeerPullPaymentCreditStatus.Aborted:
-        case PeerPullPaymentCreditStatus.Failed:
-        case PeerPullPaymentCreditStatus.Expired:
-        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
-          break;
-        default:
-          assertUnreachable(pullCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        pullCreditRec.status = newStatus;
-        const newTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        await tx.peerPullCredit.put(pullCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function abortPeerPullCreditTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullCredit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullCredit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullCredit])
-    .runReadWrite(async (tx) => {
-      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
-      if (!pullCreditRec) {
-        logger.warn(`peer pull credit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
-      switch (pullCreditRec.status) {
-        case PeerPullPaymentCreditStatus.PendingCreatePurse:
-        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
-          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
-          break;
-        case PeerPullPaymentCreditStatus.PendingWithdrawing:
-          throw Error("can't abort anymore");
-        case PeerPullPaymentCreditStatus.PendingReady:
-          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
-          break;
-        case PeerPullPaymentCreditStatus.Done:
-        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
-        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
-        case PeerPullPaymentCreditStatus.SuspendedReady:
-        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
-        case PeerPullPaymentCreditStatus.Aborted:
-        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
-        case PeerPullPaymentCreditStatus.Failed:
-        case PeerPullPaymentCreditStatus.Expired:
-        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
-          break;
-        default:
-          assertUnreachable(pullCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        pullCreditRec.status = newStatus;
-        const newTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        await tx.peerPullCredit.put(pullCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPullCreditTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullCredit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullCredit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullCredit])
-    .runReadWrite(async (tx) => {
-      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
-      if (!pullCreditRec) {
-        logger.warn(`peer pull credit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
-      switch (pullCreditRec.status) {
-        case PeerPullPaymentCreditStatus.PendingCreatePurse:
-        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
-        case PeerPullPaymentCreditStatus.PendingWithdrawing:
-        case PeerPullPaymentCreditStatus.PendingReady:
-        case PeerPullPaymentCreditStatus.Done:
-        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
-        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
-        case PeerPullPaymentCreditStatus.SuspendedReady:
-        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
-        case PeerPullPaymentCreditStatus.Aborted:
-        case PeerPullPaymentCreditStatus.Failed:
-        case PeerPullPaymentCreditStatus.Expired:
-          break;
-        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
-        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
-          newStatus = PeerPullPaymentCreditStatus.Failed;
-          break;
-        default:
-          assertUnreachable(pullCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        pullCreditRec.status = newStatus;
-        const newTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        await tx.peerPullCredit.put(pullCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumePeerPullCreditTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullCredit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullCredit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullCredit])
-    .runReadWrite(async (tx) => {
-      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
-      if (!pullCreditRec) {
-        logger.warn(`peer pull credit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
-      switch (pullCreditRec.status) {
-        case PeerPullPaymentCreditStatus.PendingCreatePurse:
-        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
-        case PeerPullPaymentCreditStatus.PendingWithdrawing:
-        case PeerPullPaymentCreditStatus.PendingReady:
-        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
-        case PeerPullPaymentCreditStatus.Done:
-        case PeerPullPaymentCreditStatus.Failed:
-        case PeerPullPaymentCreditStatus.Expired:
-        case PeerPullPaymentCreditStatus.Aborted:
-          break;
-        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
-          newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
-          break;
-        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
-          newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
-          break;
-        case PeerPullPaymentCreditStatus.SuspendedReady:
-          newStatus = PeerPullPaymentCreditStatus.PendingReady;
-          break;
-        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
-          newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
-          break;
-        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
-          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
-          break;
-        default:
-          assertUnreachable(pullCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        pullCreditRec.status = newStatus;
-        const newTxState = 
computePeerPullCreditTransactionState(pullCreditRec);
-        await tx.peerPullCredit.put(pullCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export function computePeerPullCreditTransactionState(
   pullCreditRecord: PeerPullCreditRecord,
 ): TransactionState {
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 9bbe2c875..0f9f29fb5 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
@@ -120,6 +120,73 @@ export class PeerPullDebitTransactionContext implements 
TransactionContext {
     this.peerPullDebitId = peerPullDebitId;
   }
 
+  async deleteTransaction(): Promise<void> {
+    const transactionId = this.transactionId;
+    const ws = this.ws;
+    const peerPullDebitId = this.peerPullDebitId;
+    await ws.db
+      .mktx((x) => [x.peerPullDebit, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const debit = await tx.peerPullDebit.get(peerPullDebitId);
+        if (debit) {
+          await tx.peerPullDebit.delete(peerPullDebitId);
+          await tx.tombstones.put({ id: transactionId });
+        }
+      });
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const taskId = this.taskId;
+    const transactionId = this.transactionId;
+    const ws = this.ws;
+    const peerPullDebitId = this.peerPullDebitId;
+    stopLongpolling(ws, taskId);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPullDebit])
+      .runReadWrite(async (tx) => {
+        const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
+        if (!pullDebitRec) {
+          logger.warn(`peer pull debit ${peerPullDebitId} not found`);
+          return;
+        }
+        let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
+        switch (pullDebitRec.status) {
+          case PeerPullDebitRecordStatus.DialogProposed:
+            break;
+          case PeerPullDebitRecordStatus.Done:
+            break;
+          case PeerPullDebitRecordStatus.PendingDeposit:
+            newStatus = PeerPullDebitRecordStatus.SuspendedDeposit;
+            break;
+          case PeerPullDebitRecordStatus.SuspendedDeposit:
+            break;
+          case PeerPullDebitRecordStatus.Aborted:
+            break;
+          case PeerPullDebitRecordStatus.AbortingRefresh:
+            newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh;
+            break;
+          case PeerPullDebitRecordStatus.Failed:
+            break;
+          case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+            break;
+          default:
+            assertUnreachable(pullDebitRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = 
computePeerPullDebitTransactionState(pullDebitRec);
+          pullDebitRec.status = newStatus;
+          const newTxState = 
computePeerPullDebitTransactionState(pullDebitRec);
+          await tx.peerPullDebit.put(pullDebitRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
   async resumeTransaction(): Promise<void> {
     const ctx = this;
     stopLongpolling(ctx.ws, ctx.taskId);
@@ -742,68 +809,6 @@ export async function preparePeerPullDebit(
   };
 }
 
-/**
- * FIXME: This belongs in the transaction context!
- */
-export async function suspendPeerPullDebitTransaction(
-  ws: InternalWalletState,
-  peerPullDebitId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullDebit])
-    .runReadWrite(async (tx) => {
-      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
-      if (!pullDebitRec) {
-        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
-        return;
-      }
-      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
-      switch (pullDebitRec.status) {
-        case PeerPullDebitRecordStatus.DialogProposed:
-          break;
-        case PeerPullDebitRecordStatus.Done:
-          break;
-        case PeerPullDebitRecordStatus.PendingDeposit:
-          newStatus = PeerPullDebitRecordStatus.SuspendedDeposit;
-          break;
-        case PeerPullDebitRecordStatus.SuspendedDeposit:
-          break;
-        case PeerPullDebitRecordStatus.Aborted:
-          break;
-        case PeerPullDebitRecordStatus.AbortingRefresh:
-          newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh;
-          break;
-        case PeerPullDebitRecordStatus.Failed:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
-          break;
-        default:
-          assertUnreachable(pullDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        pullDebitRec.status = newStatus;
-        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        await tx.peerPullDebit.put(pullDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export function computePeerPullDebitTransactionState(
   pullDebitRecord: PeerPullPaymentIncomingRecord,
 ): TransactionState {
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 36606e732..c8cfaac7d 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
@@ -66,6 +66,8 @@ import { checkDbInvariant } from "../util/invariants.js";
 import {
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
   constructTaskIdentifier,
   runLongpollAsync,
 } from "./common.js";
@@ -90,6 +92,268 @@ import {
 
 const logger = new Logger("pay-peer-push-credit.ts");
 
+export class PeerPushCreditTransactionContext implements TransactionContext {
+  private transactionId: string;
+  private retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public peerPushCreditId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.PeerPushCredit,
+      peerPushCreditId,
+    });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.PeerPushCredit,
+      peerPushCreditId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, peerPushCreditId } = this;
+    await ws.db.runReadWriteTx(
+      ["withdrawalGroups", "peerPushCredit", "tombstones"],
+      async (tx) => {
+        const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
+        if (!pushInc) {
+          return;
+        }
+        if (pushInc.withdrawalGroupId) {
+          const withdrawalGroupId = pushInc.withdrawalGroupId;
+          const withdrawalGroupRecord =
+            await tx.withdrawalGroups.get(withdrawalGroupId);
+          if (withdrawalGroupRecord) {
+            await tx.withdrawalGroups.delete(withdrawalGroupId);
+            await tx.tombstones.put({
+              id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+            });
+          }
+        }
+        await tx.peerPushCredit.delete(peerPushCreditId);
+        await tx.tombstones.put({
+          id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
+        });
+      },
+    );
+    return;
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, peerPushCreditId, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db.runReadWriteTx(
+      ["peerPushCredit"],
+      async (tx) => {
+        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+        if (!pushCreditRec) {
+          logger.warn(`peer push credit ${peerPushCreditId} not found`);
+          return;
+        }
+        let newStatus: PeerPushCreditStatus | undefined = undefined;
+        switch (pushCreditRec.status) {
+          case PeerPushCreditStatus.DialogProposed:
+          case PeerPushCreditStatus.Done:
+          case PeerPushCreditStatus.SuspendedMerge:
+          case PeerPushCreditStatus.SuspendedMergeKycRequired:
+          case PeerPushCreditStatus.SuspendedWithdrawing:
+            break;
+          case PeerPushCreditStatus.PendingMergeKycRequired:
+            newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
+            break;
+          case PeerPushCreditStatus.PendingMerge:
+            newStatus = PeerPushCreditStatus.SuspendedMerge;
+            break;
+          case PeerPushCreditStatus.PendingWithdrawing:
+            // FIXME: Suspend internal withdrawal transaction!
+            newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
+            break;
+          case PeerPushCreditStatus.Aborted:
+            break;
+          case PeerPushCreditStatus.Failed:
+            break;
+          default:
+            assertUnreachable(pushCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          pushCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          await tx.peerPushCredit.put(pushCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      },
+    );
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, peerPushCreditId, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db.runReadWriteTx(
+      ["peerPushCredit"],
+      async (tx) => {
+        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+        if (!pushCreditRec) {
+          logger.warn(`peer push credit ${peerPushCreditId} not found`);
+          return;
+        }
+        let newStatus: PeerPushCreditStatus | undefined = undefined;
+        switch (pushCreditRec.status) {
+          case PeerPushCreditStatus.DialogProposed:
+            newStatus = PeerPushCreditStatus.Aborted;
+            break;
+          case PeerPushCreditStatus.Done:
+            break;
+          case PeerPushCreditStatus.SuspendedMerge:
+          case PeerPushCreditStatus.SuspendedMergeKycRequired:
+          case PeerPushCreditStatus.SuspendedWithdrawing:
+            newStatus = PeerPushCreditStatus.Aborted;
+            break;
+          case PeerPushCreditStatus.PendingMergeKycRequired:
+            newStatus = PeerPushCreditStatus.Aborted;
+            break;
+          case PeerPushCreditStatus.PendingMerge:
+            newStatus = PeerPushCreditStatus.Aborted;
+            break;
+          case PeerPushCreditStatus.PendingWithdrawing:
+            newStatus = PeerPushCreditStatus.Aborted;
+            break;
+          case PeerPushCreditStatus.Aborted:
+            break;
+          case PeerPushCreditStatus.Failed:
+            break;
+          default:
+            assertUnreachable(pushCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          pushCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          await tx.peerPushCredit.put(pushCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      },
+    );
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, peerPushCreditId, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db.runReadWriteTx(
+      ["peerPushCredit"],
+      async (tx) => {
+        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+        if (!pushCreditRec) {
+          logger.warn(`peer push credit ${peerPushCreditId} not found`);
+          return;
+        }
+        let newStatus: PeerPushCreditStatus | undefined = undefined;
+        switch (pushCreditRec.status) {
+          case PeerPushCreditStatus.DialogProposed:
+          case PeerPushCreditStatus.Done:
+          case PeerPushCreditStatus.PendingMergeKycRequired:
+          case PeerPushCreditStatus.PendingMerge:
+          case PeerPushCreditStatus.PendingWithdrawing:
+          case PeerPushCreditStatus.SuspendedMerge:
+            newStatus = PeerPushCreditStatus.PendingMerge;
+            break;
+          case PeerPushCreditStatus.SuspendedMergeKycRequired:
+            newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
+            break;
+          case PeerPushCreditStatus.SuspendedWithdrawing:
+            // FIXME: resume underlying "internal-withdrawal" transaction.
+            newStatus = PeerPushCreditStatus.PendingWithdrawing;
+            break;
+          case PeerPushCreditStatus.Aborted:
+            break;
+          case PeerPushCreditStatus.Failed:
+            break;
+          default:
+            assertUnreachable(pushCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          pushCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          await tx.peerPushCredit.put(pushCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      },
+    );
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, peerPushCreditId, retryTag, transactionId } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db.runReadWriteTx(
+      ["peerPushCredit"],
+      async (tx) => {
+        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
+        if (!pushCreditRec) {
+          logger.warn(`peer push credit ${peerPushCreditId} not found`);
+          return;
+        }
+        let newStatus: PeerPushCreditStatus | undefined = undefined;
+        switch (pushCreditRec.status) {
+          case PeerPushCreditStatus.Done:
+          case PeerPushCreditStatus.Aborted:
+          case PeerPushCreditStatus.Failed:
+            // Already in a final state.
+            return;
+          case PeerPushCreditStatus.DialogProposed:
+          case PeerPushCreditStatus.PendingMergeKycRequired:
+          case PeerPushCreditStatus.PendingMerge:
+          case PeerPushCreditStatus.PendingWithdrawing:
+          case PeerPushCreditStatus.SuspendedMerge:
+          case PeerPushCreditStatus.SuspendedMergeKycRequired:
+          case PeerPushCreditStatus.SuspendedWithdrawing:
+            newStatus = PeerPushCreditStatus.Failed;
+            break;
+          default:
+            assertUnreachable(pushCreditRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          pushCreditRec.status = newStatus;
+          const newTxState =
+            computePeerPushCreditTransactionState(pushCreditRec);
+          await tx.peerPushCredit.put(pushCreditRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      },
+    );
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 export async function preparePeerPushCredit(
   ws: InternalWalletState,
   req: PreparePeerPushCreditRequest,
@@ -688,200 +952,6 @@ export async function confirmPeerPushCredit(
   };
 }
 
-export async function suspendPeerPushCreditTransaction(
-  ws: InternalWalletState,
-  peerPushCreditId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushCredit])
-    .runReadWrite(async (tx) => {
-      const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
-      if (!pushCreditRec) {
-        logger.warn(`peer push credit ${peerPushCreditId} not found`);
-        return;
-      }
-      let newStatus: PeerPushCreditStatus | undefined = undefined;
-      switch (pushCreditRec.status) {
-        case PeerPushCreditStatus.DialogProposed:
-        case PeerPushCreditStatus.Done:
-        case PeerPushCreditStatus.SuspendedMerge:
-        case PeerPushCreditStatus.SuspendedMergeKycRequired:
-        case PeerPushCreditStatus.SuspendedWithdrawing:
-          break;
-        case PeerPushCreditStatus.PendingMergeKycRequired:
-          newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
-          break;
-        case PeerPushCreditStatus.PendingMerge:
-          newStatus = PeerPushCreditStatus.SuspendedMerge;
-          break;
-        case PeerPushCreditStatus.PendingWithdrawing:
-          // FIXME: Suspend internal withdrawal transaction!
-          newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
-          break;
-        case PeerPushCreditStatus.Aborted:
-          break;
-        case PeerPushCreditStatus.Failed:
-          break;
-        default:
-          assertUnreachable(pushCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        pushCreditRec.status = newStatus;
-        const newTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        await tx.peerPushCredit.put(pushCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function abortPeerPushCreditTransaction(
-  ws: InternalWalletState,
-  peerPushCreditId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushCredit])
-    .runReadWrite(async (tx) => {
-      const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
-      if (!pushCreditRec) {
-        logger.warn(`peer push credit ${peerPushCreditId} not found`);
-        return;
-      }
-      let newStatus: PeerPushCreditStatus | undefined = undefined;
-      switch (pushCreditRec.status) {
-        case PeerPushCreditStatus.DialogProposed:
-          newStatus = PeerPushCreditStatus.Aborted;
-          break;
-        case PeerPushCreditStatus.Done:
-          break;
-        case PeerPushCreditStatus.SuspendedMerge:
-        case PeerPushCreditStatus.SuspendedMergeKycRequired:
-        case PeerPushCreditStatus.SuspendedWithdrawing:
-          newStatus = PeerPushCreditStatus.Aborted;
-          break;
-        case PeerPushCreditStatus.PendingMergeKycRequired:
-          newStatus = PeerPushCreditStatus.Aborted;
-          break;
-        case PeerPushCreditStatus.PendingMerge:
-          newStatus = PeerPushCreditStatus.Aborted;
-          break;
-        case PeerPushCreditStatus.PendingWithdrawing:
-          newStatus = PeerPushCreditStatus.Aborted;
-          break;
-        case PeerPushCreditStatus.Aborted:
-          break;
-        case PeerPushCreditStatus.Failed:
-          break;
-        default:
-          assertUnreachable(pushCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        pushCreditRec.status = newStatus;
-        const newTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        await tx.peerPushCredit.put(pushCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPushCreditTransaction(
-  ws: InternalWalletState,
-  peerPushCreditId: string,
-) {
-  // We don't have any "aborting" states!
-  throw Error("can't run cancel-aborting on peer-push-credit transaction");
-}
-
-export async function resumePeerPushCreditTransaction(
-  ws: InternalWalletState,
-  peerPushCreditId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushCredit,
-    peerPushCreditId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushCredit])
-    .runReadWrite(async (tx) => {
-      const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
-      if (!pushCreditRec) {
-        logger.warn(`peer push credit ${peerPushCreditId} not found`);
-        return;
-      }
-      let newStatus: PeerPushCreditStatus | undefined = undefined;
-      switch (pushCreditRec.status) {
-        case PeerPushCreditStatus.DialogProposed:
-        case PeerPushCreditStatus.Done:
-        case PeerPushCreditStatus.PendingMergeKycRequired:
-        case PeerPushCreditStatus.PendingMerge:
-        case PeerPushCreditStatus.PendingWithdrawing:
-        case PeerPushCreditStatus.SuspendedMerge:
-          newStatus = PeerPushCreditStatus.PendingMerge;
-          break;
-        case PeerPushCreditStatus.SuspendedMergeKycRequired:
-          newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
-          break;
-        case PeerPushCreditStatus.SuspendedWithdrawing:
-          // FIXME: resume underlying "internal-withdrawal" transaction.
-          newStatus = PeerPushCreditStatus.PendingWithdrawing;
-          break;
-        case PeerPushCreditStatus.Aborted:
-          break;
-        case PeerPushCreditStatus.Failed:
-          break;
-        default:
-          assertUnreachable(pushCreditRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        pushCreditRec.status = newStatus;
-        const newTxState = 
computePeerPushCreditTransactionState(pushCreditRec);
-        await tx.peerPushCredit.put(pushCreditRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export function computePeerPushCreditTransactionState(
   pushCreditRecord: PeerPushPaymentIncomingRecord,
 ): TransactionState {
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 a11ffe774..4fd1ef3b2 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
@@ -63,6 +63,7 @@ import { checkLogicInvariant } from "../util/invariants.js";
 import {
   TaskRunResult,
   TaskRunResultType,
+  TransactionContext,
   constructTaskIdentifier,
   runLongpollAsync,
   spendCoins,
@@ -80,6 +81,257 @@ import {
 
 const logger = new Logger("pay-peer-push-debit.ts");
 
+export class PeerPushDebitTransactionContext implements TransactionContext {
+  public transactionId: string;
+  public retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public pursePub: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.PeerPushDebit,
+      pursePub,
+    });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.PeerPushDebit,
+      pursePub,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, pursePub, transactionId } = this;
+    await ws.db
+      .mktx((x) => [x.peerPushDebit, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const debit = await tx.peerPushDebit.get(pursePub);
+        if (debit) {
+          await tx.peerPushDebit.delete(pursePub);
+          await tx.tombstones.put({ id: transactionId });
+        }
+      });
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, pursePub, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPushDebit])
+      .runReadWrite(async (tx) => {
+        const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+        if (!pushDebitRec) {
+          logger.warn(`peer push debit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPushDebitStatus | undefined = undefined;
+        switch (pushDebitRec.status) {
+          case PeerPushDebitStatus.PendingCreatePurse:
+            newStatus = PeerPushDebitStatus.SuspendedCreatePurse;
+            break;
+          case PeerPushDebitStatus.AbortingRefreshDeleted:
+            newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted;
+            break;
+          case PeerPushDebitStatus.AbortingRefreshExpired:
+            newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired;
+            break;
+          case PeerPushDebitStatus.AbortingDeletePurse:
+            newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse;
+            break;
+          case PeerPushDebitStatus.PendingReady:
+            newStatus = PeerPushDebitStatus.SuspendedReady;
+            break;
+          case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+          case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+          case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+          case PeerPushDebitStatus.SuspendedReady:
+          case PeerPushDebitStatus.SuspendedCreatePurse:
+          case PeerPushDebitStatus.Done:
+          case PeerPushDebitStatus.Aborted:
+          case PeerPushDebitStatus.Failed:
+          case PeerPushDebitStatus.Expired:
+            // Do nothing
+            break;
+          default:
+            assertUnreachable(pushDebitRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          pushDebitRec.status = newStatus;
+          const newTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          await tx.peerPushDebit.put(pushDebitRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, pursePub, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPushDebit])
+      .runReadWrite(async (tx) => {
+        const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+        if (!pushDebitRec) {
+          logger.warn(`peer push debit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPushDebitStatus | undefined = undefined;
+        switch (pushDebitRec.status) {
+          case PeerPushDebitStatus.PendingReady:
+          case PeerPushDebitStatus.SuspendedReady:
+            newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+            break;
+          case PeerPushDebitStatus.SuspendedCreatePurse:
+          case PeerPushDebitStatus.PendingCreatePurse:
+            // Network request might already be in-flight!
+            newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+            break;
+          case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+          case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+          case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+          case PeerPushDebitStatus.AbortingRefreshDeleted:
+          case PeerPushDebitStatus.AbortingRefreshExpired:
+          case PeerPushDebitStatus.Done:
+          case PeerPushDebitStatus.AbortingDeletePurse:
+          case PeerPushDebitStatus.Aborted:
+          case PeerPushDebitStatus.Expired:
+          case PeerPushDebitStatus.Failed:
+            // Do nothing
+            break;
+          default:
+            assertUnreachable(pushDebitRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          pushDebitRec.status = newStatus;
+          const newTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          await tx.peerPushDebit.put(pushDebitRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, pursePub, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPushDebit])
+      .runReadWrite(async (tx) => {
+        const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+        if (!pushDebitRec) {
+          logger.warn(`peer push debit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPushDebitStatus | undefined = undefined;
+        switch (pushDebitRec.status) {
+          case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+            newStatus = PeerPushDebitStatus.AbortingDeletePurse;
+            break;
+          case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+            newStatus = PeerPushDebitStatus.AbortingRefreshDeleted;
+            break;
+          case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+            newStatus = PeerPushDebitStatus.AbortingRefreshExpired;
+            break;
+          case PeerPushDebitStatus.SuspendedReady:
+            newStatus = PeerPushDebitStatus.PendingReady;
+            break;
+          case PeerPushDebitStatus.SuspendedCreatePurse:
+            newStatus = PeerPushDebitStatus.PendingCreatePurse;
+            break;
+          case PeerPushDebitStatus.PendingCreatePurse:
+          case PeerPushDebitStatus.AbortingRefreshDeleted:
+          case PeerPushDebitStatus.AbortingRefreshExpired:
+          case PeerPushDebitStatus.AbortingDeletePurse:
+          case PeerPushDebitStatus.PendingReady:
+          case PeerPushDebitStatus.Done:
+          case PeerPushDebitStatus.Aborted:
+          case PeerPushDebitStatus.Failed:
+          case PeerPushDebitStatus.Expired:
+            // Do nothing
+            break;
+          default:
+            assertUnreachable(pushDebitRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          pushDebitRec.status = newStatus;
+          const newTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          await tx.peerPushDebit.put(pushDebitRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, pursePub, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.peerPushDebit])
+      .runReadWrite(async (tx) => {
+        const pushDebitRec = await tx.peerPushDebit.get(pursePub);
+        if (!pushDebitRec) {
+          logger.warn(`peer push debit ${pursePub} not found`);
+          return;
+        }
+        let newStatus: PeerPushDebitStatus | undefined = undefined;
+        switch (pushDebitRec.status) {
+          case PeerPushDebitStatus.AbortingRefreshDeleted:
+          case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
+            // FIXME: What to do about the refresh group?
+            newStatus = PeerPushDebitStatus.Failed;
+            break;
+          case PeerPushDebitStatus.AbortingDeletePurse:
+          case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+          case PeerPushDebitStatus.AbortingRefreshExpired:
+          case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
+          case PeerPushDebitStatus.PendingReady:
+          case PeerPushDebitStatus.SuspendedReady:
+          case PeerPushDebitStatus.SuspendedCreatePurse:
+          case PeerPushDebitStatus.PendingCreatePurse:
+            newStatus = PeerPushDebitStatus.Failed;
+            break;
+          case PeerPushDebitStatus.Done:
+          case PeerPushDebitStatus.Aborted:
+          case PeerPushDebitStatus.Failed:
+          case PeerPushDebitStatus.Expired:
+            // Do nothing
+            break;
+          default:
+            assertUnreachable(pushDebitRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          pushDebitRec.status = newStatus;
+          const newTxState = 
computePeerPushDebitTransactionState(pushDebitRec);
+          await tx.peerPushDebit.put(pushDebitRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 export async function checkPeerPushDebit(
   ws: InternalWalletState,
   req: CheckPeerPushDebitRequest,
@@ -118,8 +370,9 @@ async function handlePurseCreationConflict(
 ): Promise<TaskRunResult> {
   const pursePub = peerPushInitiation.pursePub;
   const errResp = await readTalerErrorResponse(resp);
+  const ctx = new PeerPushDebitTransactionContext(ws, pursePub);
   if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
-    await failPeerPushDebitTransaction(ws, pursePub);
+    await ctx.failTransaction();
     return TaskRunResult.finished();
   }
 
@@ -189,10 +442,8 @@ async function processPeerPushDebitCreateReserve(
   const pursePub = peerPushInitiation.pursePub;
   const purseExpiration = peerPushInitiation.purseExpiration;
   const hContractTerms = peerPushInitiation.contractTermsHash;
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushDebit,
-    pursePub: pursePub,
-  });
+  const ctx = new PeerPushDebitTransactionContext(ws, pursePub);
+  const transactionId = ctx.transactionId;
 
   logger.trace(`processing ${transactionId} pending(create-reserve)`);
 
@@ -277,7 +528,7 @@ async function processPeerPushDebitCreateReserve(
       break;
     case HttpStatusCode.Forbidden: {
       // FIXME: Store this error!
-      await failPeerPushDebitTransaction(ws, pursePub);
+      await ctx.failTransaction();
       return TaskRunResult.finished();
     }
     case HttpStatusCode.Conflict: {
@@ -838,265 +1089,6 @@ export function computePeerPushDebitTransactionActions(
   }
 }
 
-export async function abortPeerPushDebitTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushDebit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushDebit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushDebit])
-    .runReadWrite(async (tx) => {
-      const pushDebitRec = await tx.peerPushDebit.get(pursePub);
-      if (!pushDebitRec) {
-        logger.warn(`peer push debit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPushDebitStatus | undefined = undefined;
-      switch (pushDebitRec.status) {
-        case PeerPushDebitStatus.PendingReady:
-        case PeerPushDebitStatus.SuspendedReady:
-          newStatus = PeerPushDebitStatus.AbortingDeletePurse;
-          break;
-        case PeerPushDebitStatus.SuspendedCreatePurse:
-        case PeerPushDebitStatus.PendingCreatePurse:
-          // Network request might already be in-flight!
-          newStatus = PeerPushDebitStatus.AbortingDeletePurse;
-          break;
-        case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
-        case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
-        case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
-        case PeerPushDebitStatus.AbortingRefreshDeleted:
-        case PeerPushDebitStatus.AbortingRefreshExpired:
-        case PeerPushDebitStatus.Done:
-        case PeerPushDebitStatus.AbortingDeletePurse:
-        case PeerPushDebitStatus.Aborted:
-        case PeerPushDebitStatus.Expired:
-        case PeerPushDebitStatus.Failed:
-          // Do nothing
-          break;
-        default:
-          assertUnreachable(pushDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        pushDebitRec.status = newStatus;
-        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        await tx.peerPushDebit.put(pushDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPushDebitTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushDebit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushDebit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushDebit])
-    .runReadWrite(async (tx) => {
-      const pushDebitRec = await tx.peerPushDebit.get(pursePub);
-      if (!pushDebitRec) {
-        logger.warn(`peer push debit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPushDebitStatus | undefined = undefined;
-      switch (pushDebitRec.status) {
-        case PeerPushDebitStatus.AbortingRefreshDeleted:
-        case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
-          // FIXME: What to do about the refresh group?
-          newStatus = PeerPushDebitStatus.Failed;
-          break;
-        case PeerPushDebitStatus.AbortingDeletePurse:
-        case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
-        case PeerPushDebitStatus.AbortingRefreshExpired:
-        case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
-        case PeerPushDebitStatus.PendingReady:
-        case PeerPushDebitStatus.SuspendedReady:
-        case PeerPushDebitStatus.SuspendedCreatePurse:
-        case PeerPushDebitStatus.PendingCreatePurse:
-          newStatus = PeerPushDebitStatus.Failed;
-          break;
-        case PeerPushDebitStatus.Done:
-        case PeerPushDebitStatus.Aborted:
-        case PeerPushDebitStatus.Failed:
-        case PeerPushDebitStatus.Expired:
-          // Do nothing
-          break;
-        default:
-          assertUnreachable(pushDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        pushDebitRec.status = newStatus;
-        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        await tx.peerPushDebit.put(pushDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function suspendPeerPushDebitTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushDebit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushDebit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushDebit])
-    .runReadWrite(async (tx) => {
-      const pushDebitRec = await tx.peerPushDebit.get(pursePub);
-      if (!pushDebitRec) {
-        logger.warn(`peer push debit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPushDebitStatus | undefined = undefined;
-      switch (pushDebitRec.status) {
-        case PeerPushDebitStatus.PendingCreatePurse:
-          newStatus = PeerPushDebitStatus.SuspendedCreatePurse;
-          break;
-        case PeerPushDebitStatus.AbortingRefreshDeleted:
-          newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted;
-          break;
-        case PeerPushDebitStatus.AbortingRefreshExpired:
-          newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired;
-          break;
-        case PeerPushDebitStatus.AbortingDeletePurse:
-          newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse;
-          break;
-        case PeerPushDebitStatus.PendingReady:
-          newStatus = PeerPushDebitStatus.SuspendedReady;
-          break;
-        case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
-        case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
-        case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
-        case PeerPushDebitStatus.SuspendedReady:
-        case PeerPushDebitStatus.SuspendedCreatePurse:
-        case PeerPushDebitStatus.Done:
-        case PeerPushDebitStatus.Aborted:
-        case PeerPushDebitStatus.Failed:
-        case PeerPushDebitStatus.Expired:
-          // Do nothing
-          break;
-        default:
-          assertUnreachable(pushDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        pushDebitRec.status = newStatus;
-        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        await tx.peerPushDebit.put(pushDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumePeerPushDebitTransaction(
-  ws: InternalWalletState,
-  pursePub: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPushDebit,
-    pursePub,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPushDebit,
-    pursePub,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPushDebit])
-    .runReadWrite(async (tx) => {
-      const pushDebitRec = await tx.peerPushDebit.get(pursePub);
-      if (!pushDebitRec) {
-        logger.warn(`peer push debit ${pursePub} not found`);
-        return;
-      }
-      let newStatus: PeerPushDebitStatus | undefined = undefined;
-      switch (pushDebitRec.status) {
-        case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
-          newStatus = PeerPushDebitStatus.AbortingDeletePurse;
-          break;
-        case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted:
-          newStatus = PeerPushDebitStatus.AbortingRefreshDeleted;
-          break;
-        case PeerPushDebitStatus.SuspendedAbortingRefreshExpired:
-          newStatus = PeerPushDebitStatus.AbortingRefreshExpired;
-          break;
-        case PeerPushDebitStatus.SuspendedReady:
-          newStatus = PeerPushDebitStatus.PendingReady;
-          break;
-        case PeerPushDebitStatus.SuspendedCreatePurse:
-          newStatus = PeerPushDebitStatus.PendingCreatePurse;
-          break;
-        case PeerPushDebitStatus.PendingCreatePurse:
-        case PeerPushDebitStatus.AbortingRefreshDeleted:
-        case PeerPushDebitStatus.AbortingRefreshExpired:
-        case PeerPushDebitStatus.AbortingDeletePurse:
-        case PeerPushDebitStatus.PendingReady:
-        case PeerPushDebitStatus.Done:
-        case PeerPushDebitStatus.Aborted:
-        case PeerPushDebitStatus.Failed:
-        case PeerPushDebitStatus.Expired:
-          // Do nothing
-          break;
-        default:
-          assertUnreachable(pushDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        pushDebitRec.status = newStatus;
-        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
-        await tx.peerPushDebit.put(pushDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export function computePeerPushDebitTransactionState(
   ppiRecord: PeerPushDebitRecord,
 ): TransactionState {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 390433f66..fc2508cd3 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -98,6 +98,8 @@ import {
   makeCoinsVisible,
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
 } from "./common.js";
 import { fetchFreshExchange } from "./exchanges.js";
 import {
@@ -107,6 +109,152 @@ import {
 
 const logger = new Logger("refresh.ts");
 
+export class RefreshTransactionContext implements TransactionContext {
+  public transactionId: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public refreshGroupId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Refresh,
+      refreshGroupId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const refreshGroupId = this.refreshGroupId;
+    const ws = this.ws;
+    await ws.db
+      .mktx((x) => [x.refreshGroups, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const rg = await tx.refreshGroups.get(refreshGroupId);
+        if (rg) {
+          await tx.refreshGroups.delete(refreshGroupId);
+          await tx.tombstones.put({
+            id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
+          });
+        }
+      });
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, refreshGroupId, transactionId } = this;
+    let res = await ws.db
+      .mktx((x) => [x.refreshGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.refreshGroups.get(refreshGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't suspend refresh group, refreshGroupId=${refreshGroupId} not 
found`,
+          );
+          return undefined;
+        }
+        const oldState = computeRefreshTransactionState(dg);
+        switch (dg.operationStatus) {
+          case RefreshOperationStatus.Finished:
+            return undefined;
+          case RefreshOperationStatus.Pending: {
+            dg.operationStatus = RefreshOperationStatus.Suspended;
+            await tx.refreshGroups.put(dg);
+            return {
+              oldTxState: oldState,
+              newTxState: computeRefreshTransactionState(dg),
+            };
+          }
+          case RefreshOperationStatus.Suspended:
+            return undefined;
+        }
+        return undefined;
+      });
+    if (res) {
+      ws.notify({
+        type: NotificationType.TransactionStateTransition,
+        transactionId,
+        oldTxState: res.oldTxState,
+        newTxState: res.newTxState,
+      });
+    }
+  }
+
+  async abortTransaction(): Promise<void> {
+    // Refresh transactions only support fail, not abort.
+    throw new Error("refresh transactions cannot be aborted");
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, refreshGroupId, transactionId } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.refreshGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.refreshGroups.get(refreshGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't resume refresh group, refreshGroupId=${refreshGroupId} not 
found`,
+          );
+          return;
+        }
+        const oldState = computeRefreshTransactionState(dg);
+        switch (dg.operationStatus) {
+          case RefreshOperationStatus.Finished:
+            return;
+          case RefreshOperationStatus.Pending: {
+            return;
+          }
+          case RefreshOperationStatus.Suspended:
+            dg.operationStatus = RefreshOperationStatus.Pending;
+            await tx.refreshGroups.put(dg);
+            return {
+              oldTxState: oldState,
+              newTxState: computeRefreshTransactionState(dg),
+            };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, refreshGroupId, transactionId } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.refreshGroups])
+      .runReadWrite(async (tx) => {
+        const dg = await tx.refreshGroups.get(refreshGroupId);
+        if (!dg) {
+          logger.warn(
+            `can't resume refresh group, refreshGroupId=${refreshGroupId} not 
found`,
+          );
+          return;
+        }
+        const oldState = computeRefreshTransactionState(dg);
+        let newStatus: RefreshOperationStatus | undefined;
+        switch (dg.operationStatus) {
+          case RefreshOperationStatus.Finished:
+            break;
+          case RefreshOperationStatus.Pending:
+          case RefreshOperationStatus.Suspended:
+            newStatus = RefreshOperationStatus.Failed;
+            break;
+          case RefreshOperationStatus.Failed:
+            break;
+          default:
+            assertUnreachable(dg.operationStatus);
+        }
+        if (newStatus) {
+          dg.operationStatus = newStatus;
+          await tx.refreshGroups.put(dg);
+        }
+        return {
+          oldTxState: oldState,
+          newTxState: computeRefreshTransactionState(dg),
+        };
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 /**
  * Get the amount that we lose when refreshing a coin of the given denomination
  * with a certain amount left.
@@ -1256,9 +1404,6 @@ export async function autoRefresh(
           `created refresh group for auto-refresh (${res.refreshGroupId})`,
         );
       }
-      //      logger.trace(
-      //        `current wallet time: 
${AbsoluteTime.toIsoString(AbsoluteTime.now())}`,
-      //      );
       logger.trace(
         `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`,
       );
@@ -1308,142 +1453,6 @@ export function computeRefreshTransactionActions(
   }
 }
 
-export async function suspendRefreshGroup(
-  ws: InternalWalletState,
-  refreshGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Refresh,
-    refreshGroupId,
-  });
-  let res = await ws.db
-    .mktx((x) => [x.refreshGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.refreshGroups.get(refreshGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't suspend refresh group, refreshGroupId=${refreshGroupId} not 
found`,
-        );
-        return undefined;
-      }
-      const oldState = computeRefreshTransactionState(dg);
-      switch (dg.operationStatus) {
-        case RefreshOperationStatus.Finished:
-          return undefined;
-        case RefreshOperationStatus.Pending: {
-          dg.operationStatus = RefreshOperationStatus.Suspended;
-          await tx.refreshGroups.put(dg);
-          return {
-            oldTxState: oldState,
-            newTxState: computeRefreshTransactionState(dg),
-          };
-        }
-        case RefreshOperationStatus.Suspended:
-          return undefined;
-      }
-      return undefined;
-    });
-  if (res) {
-    ws.notify({
-      type: NotificationType.TransactionStateTransition,
-      transactionId,
-      oldTxState: res.oldTxState,
-      newTxState: res.newTxState,
-    });
-  }
-}
-
-export async function resumeRefreshGroup(
-  ws: InternalWalletState,
-  refreshGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Refresh,
-    refreshGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.refreshGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.refreshGroups.get(refreshGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't resume refresh group, refreshGroupId=${refreshGroupId} not 
found`,
-        );
-        return;
-      }
-      const oldState = computeRefreshTransactionState(dg);
-      switch (dg.operationStatus) {
-        case RefreshOperationStatus.Finished:
-          return;
-        case RefreshOperationStatus.Pending: {
-          return;
-        }
-        case RefreshOperationStatus.Suspended:
-          dg.operationStatus = RefreshOperationStatus.Pending;
-          await tx.refreshGroups.put(dg);
-          return {
-            oldTxState: oldState,
-            newTxState: computeRefreshTransactionState(dg),
-          };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failRefreshGroup(
-  ws: InternalWalletState,
-  refreshGroupId: string,
-): Promise<void> {
-  throw Error("action cancel-aborting not allowed on refreshes");
-}
-
-export async function abortRefreshGroup(
-  ws: InternalWalletState,
-  refreshGroupId: string,
-): Promise<void> {
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Refresh,
-    refreshGroupId,
-  });
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.refreshGroups])
-    .runReadWrite(async (tx) => {
-      const dg = await tx.refreshGroups.get(refreshGroupId);
-      if (!dg) {
-        logger.warn(
-          `can't resume refresh group, refreshGroupId=${refreshGroupId} not 
found`,
-        );
-        return;
-      }
-      const oldState = computeRefreshTransactionState(dg);
-      let newStatus: RefreshOperationStatus | undefined;
-      switch (dg.operationStatus) {
-        case RefreshOperationStatus.Finished:
-          break;
-        case RefreshOperationStatus.Pending:
-        case RefreshOperationStatus.Suspended:
-          newStatus = RefreshOperationStatus.Failed;
-          break;
-        case RefreshOperationStatus.Failed:
-          break;
-        default:
-          assertUnreachable(dg.operationStatus);
-      }
-      if (newStatus) {
-        dg.operationStatus = newStatus;
-        await tx.refreshGroups.put(dg);
-      }
-      return {
-        oldTxState: oldState,
-        newTxState: computeRefreshTransactionState(dg),
-      };
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export async function forceRefresh(
   ws: InternalWalletState,
   req: ForceRefreshRequest,
diff --git a/packages/taler-wallet-core/src/operations/reward.ts 
b/packages/taler-wallet-core/src/operations/reward.ts
index 79beb6432..62ac81d7f 100644
--- a/packages/taler-wallet-core/src/operations/reward.ts
+++ b/packages/taler-wallet-core/src/operations/reward.ts
@@ -68,6 +68,8 @@ import {
   makeCoinsVisible,
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
 } from "./common.js";
 import { fetchFreshExchange } from "./exchanges.js";
 import {
@@ -86,6 +88,202 @@ import { assertUnreachable } from 
"../util/assertUnreachable.js";
 
 const logger = new Logger("operations/tip.ts");
 
+export class RewardTransactionContext implements TransactionContext {
+  public transactionId: string;
+  public retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public walletRewardId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Reward,
+      walletRewardId,
+    });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.RewardPickup,
+      walletRewardId,
+    });
+  }
+
+  async deleteTransaction(): Promise<void> {
+    const { ws, walletRewardId } = this;
+    await ws.db
+      .mktx((x) => [x.rewards, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const tipRecord = await tx.rewards.get(walletRewardId);
+        if (tipRecord) {
+          await tx.rewards.delete(walletRewardId);
+          await tx.tombstones.put({
+            id: TombstoneTag.DeleteReward + ":" + walletRewardId,
+          });
+        }
+      });
+  }
+
+  async suspendTransaction(): Promise<void> {
+    const { ws, walletRewardId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.rewards])
+      .runReadWrite(async (tx) => {
+        const tipRec = await tx.rewards.get(walletRewardId);
+        if (!tipRec) {
+          logger.warn(`transaction tip ${walletRewardId} not found`);
+          return;
+        }
+        let newStatus: RewardRecordStatus | undefined = undefined;
+        switch (tipRec.status) {
+          case RewardRecordStatus.Done:
+          case RewardRecordStatus.SuspendedPickup:
+          case RewardRecordStatus.Aborted:
+          case RewardRecordStatus.DialogAccept:
+          case RewardRecordStatus.Failed:
+            break;
+          case RewardRecordStatus.PendingPickup:
+            newStatus = RewardRecordStatus.SuspendedPickup;
+            break;
+
+          default:
+            assertUnreachable(tipRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = computeRewardTransactionStatus(tipRec);
+          tipRec.status = newStatus;
+          const newTxState = computeRewardTransactionStatus(tipRec);
+          await tx.rewards.put(tipRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async abortTransaction(): Promise<void> {
+    const { ws, walletRewardId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.rewards])
+      .runReadWrite(async (tx) => {
+        const tipRec = await tx.rewards.get(walletRewardId);
+        if (!tipRec) {
+          logger.warn(`transaction tip ${walletRewardId} not found`);
+          return;
+        }
+        let newStatus: RewardRecordStatus | undefined = undefined;
+        switch (tipRec.status) {
+          case RewardRecordStatus.Done:
+          case RewardRecordStatus.Aborted:
+          case RewardRecordStatus.PendingPickup:
+          case RewardRecordStatus.DialogAccept:
+          case RewardRecordStatus.Failed:
+            break;
+          case RewardRecordStatus.SuspendedPickup:
+            newStatus = RewardRecordStatus.Aborted;
+            break;
+          default:
+            assertUnreachable(tipRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = computeRewardTransactionStatus(tipRec);
+          tipRec.status = newStatus;
+          const newTxState = computeRewardTransactionStatus(tipRec);
+          await tx.rewards.put(tipRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, walletRewardId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.rewards])
+      .runReadWrite(async (tx) => {
+        const rewardRec = await tx.rewards.get(walletRewardId);
+        if (!rewardRec) {
+          logger.warn(`transaction reward ${walletRewardId} not found`);
+          return;
+        }
+        let newStatus: RewardRecordStatus | undefined = undefined;
+        switch (rewardRec.status) {
+          case RewardRecordStatus.Done:
+          case RewardRecordStatus.PendingPickup:
+          case RewardRecordStatus.Aborted:
+          case RewardRecordStatus.DialogAccept:
+          case RewardRecordStatus.Failed:
+            break;
+          case RewardRecordStatus.SuspendedPickup:
+            newStatus = RewardRecordStatus.PendingPickup;
+            break;
+          default:
+            assertUnreachable(rewardRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = computeRewardTransactionStatus(rewardRec);
+          rewardRec.status = newStatus;
+          const newTxState = computeRewardTransactionStatus(rewardRec);
+          await tx.rewards.put(rewardRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, walletRewardId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.rewards])
+      .runReadWrite(async (tx) => {
+        const tipRec = await tx.rewards.get(walletRewardId);
+        if (!tipRec) {
+          logger.warn(`transaction tip ${walletRewardId} not found`);
+          return;
+        }
+        let newStatus: RewardRecordStatus | undefined = undefined;
+        switch (tipRec.status) {
+          case RewardRecordStatus.Done:
+          case RewardRecordStatus.Aborted:
+          case RewardRecordStatus.Failed:
+            break;
+          case RewardRecordStatus.PendingPickup:
+          case RewardRecordStatus.DialogAccept:
+          case RewardRecordStatus.SuspendedPickup:
+            newStatus = RewardRecordStatus.Failed;
+            break;
+          default:
+            assertUnreachable(tipRec.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = computeRewardTransactionStatus(tipRec);
+          tipRec.status = newStatus;
+          const newTxState = computeRewardTransactionStatus(tipRec);
+          await tx.rewards.put(tipRec);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+}
+
 /**
  * Get the (DD37-style) transaction status based on the
  * database record of a reward.
@@ -117,6 +315,10 @@ export function computeRewardTransactionStatus(
         major: TransactionMajorState.Pending,
         minor: TransactionMinorState.Pickup,
       };
+    case RewardRecordStatus.Failed:
+      return {
+        major: TransactionMajorState.Failed,
+      };
     default:
       assertUnreachable(tipRecord.status);
   }
@@ -128,6 +330,8 @@ export function computeTipTransactionActions(
   switch (tipRecord.status) {
     case RewardRecordStatus.Done:
       return [TransactionAction.Delete];
+    case RewardRecordStatus.Failed:
+      return [TransactionAction.Delete];
     case RewardRecordStatus.Aborted:
       return [TransactionAction.Delete];
     case RewardRecordStatus.PendingPickup:
@@ -141,7 +345,7 @@ export function computeTipTransactionActions(
   }
 }
 
-export async function prepareTip(
+export async function prepareReward(
   ws: InternalWalletState,
   talerTipUri: string,
 ): Promise<PrepareTipResult> {
@@ -166,33 +370,33 @@ export async function prepareTip(
     );
     logger.trace("checking tip status from", tipStatusUrl.href);
     const merchantResp = await ws.http.fetch(tipStatusUrl.href);
-    const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+    const rewardPickupStatus = await readSuccessResponseJsonOrThrow(
       merchantResp,
       codecForRewardPickupGetResponse(),
     );
-    logger.trace(`status ${j2s(tipPickupStatus)}`);
+    logger.trace(`status ${j2s(rewardPickupStatus)}`);
 
-    const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount);
+    const amount = Amounts.parseOrThrow(rewardPickupStatus.reward_amount);
     const currency = amount.currency;
 
     logger.trace("new tip, creating tip record");
-    await fetchFreshExchange(ws, tipPickupStatus.exchange_url);
+    await fetchFreshExchange(ws, rewardPickupStatus.exchange_url);
 
     //FIXME: is this needed? withdrawDetails is not used
     // * if the intention is to update the exchange information in the database
     //   maybe we can use another name. `get` seems like a pure-function
     const withdrawDetails = await getExchangeWithdrawalInfo(
       ws,
-      tipPickupStatus.exchange_url,
+      rewardPickupStatus.exchange_url,
       amount,
       undefined,
     );
 
-    const walletTipId = encodeCrock(getRandomBytes(32));
-    await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
+    const walletRewardId = encodeCrock(getRandomBytes(32));
+    await updateWithdrawalDenoms(ws, rewardPickupStatus.exchange_url);
     const denoms = await getCandidateWithdrawalDenoms(
       ws,
-      tipPickupStatus.exchange_url,
+      rewardPickupStatus.exchange_url,
       currency,
     );
     const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
@@ -201,13 +405,13 @@ export async function prepareTip(
     const denomSelUid = encodeCrock(getRandomBytes(32));
 
     const newTipRecord: RewardRecord = {
-      walletRewardId: walletTipId,
+      walletRewardId: walletRewardId,
       acceptedTimestamp: undefined,
       status: RewardRecordStatus.DialogAccept,
       rewardAmountRaw: Amounts.stringify(amount),
-      rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration),
-      exchangeBaseUrl: tipPickupStatus.exchange_url,
-      next_url: tipPickupStatus.next_url,
+      rewardExpiration: timestampProtocolToDb(rewardPickupStatus.expiration),
+      exchangeBaseUrl: rewardPickupStatus.exchange_url,
+      next_url: rewardPickupStatus.next_url,
       merchantBaseUrl: res.merchantBaseUrl,
       createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
       merchantRewardId: res.merchantRewardId,
@@ -485,160 +689,3 @@ export async function acceptTip(
     next_url: tipRecord.next_url,
   };
 }
-
-export async function suspendRewardTransaction(
-  ws: InternalWalletState,
-  walletRewardId: string,
-): Promise<void> {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.RewardPickup,
-    walletRewardId: walletRewardId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Reward,
-    walletRewardId: walletRewardId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.rewards])
-    .runReadWrite(async (tx) => {
-      const tipRec = await tx.rewards.get(walletRewardId);
-      if (!tipRec) {
-        logger.warn(`transaction tip ${walletRewardId} not found`);
-        return;
-      }
-      let newStatus: RewardRecordStatus | undefined = undefined;
-      switch (tipRec.status) {
-        case RewardRecordStatus.Done:
-        case RewardRecordStatus.SuspendedPickup:
-        case RewardRecordStatus.Aborted:
-        case RewardRecordStatus.DialogAccept:
-          break;
-        case RewardRecordStatus.PendingPickup:
-          newStatus = RewardRecordStatus.SuspendedPickup;
-          break;
-
-        default:
-          assertUnreachable(tipRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computeRewardTransactionStatus(tipRec);
-        tipRec.status = newStatus;
-        const newTxState = computeRewardTransactionStatus(tipRec);
-        await tx.rewards.put(tipRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumeTipTransaction(
-  ws: InternalWalletState,
-  walletRewardId: string,
-): Promise<void> {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.RewardPickup,
-    walletRewardId: walletRewardId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Reward,
-    walletRewardId: walletRewardId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.rewards])
-    .runReadWrite(async (tx) => {
-      const rewardRec = await tx.rewards.get(walletRewardId);
-      if (!rewardRec) {
-        logger.warn(`transaction reward ${walletRewardId} not found`);
-        return;
-      }
-      let newStatus: RewardRecordStatus | undefined = undefined;
-      switch (rewardRec.status) {
-        case RewardRecordStatus.Done:
-        case RewardRecordStatus.PendingPickup:
-        case RewardRecordStatus.Aborted:
-        case RewardRecordStatus.DialogAccept:
-          break;
-        case RewardRecordStatus.SuspendedPickup:
-          newStatus = RewardRecordStatus.PendingPickup;
-          break;
-        default:
-          assertUnreachable(rewardRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computeRewardTransactionStatus(rewardRec);
-        rewardRec.status = newStatus;
-        const newTxState = computeRewardTransactionStatus(rewardRec);
-        await tx.rewards.put(rewardRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failTipTransaction(
-  ws: InternalWalletState,
-  walletTipId: string,
-): Promise<void> {
-  // We don't have an "aborting" state, so this should never happen!
-  throw Error("can't run cance-aborting on tip transaction");
-}
-
-export async function abortTipTransaction(
-  ws: InternalWalletState,
-  walletRewardId: string,
-): Promise<void> {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.RewardPickup,
-    walletRewardId: walletRewardId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Reward,
-    walletRewardId: walletRewardId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.rewards])
-    .runReadWrite(async (tx) => {
-      const tipRec = await tx.rewards.get(walletRewardId);
-      if (!tipRec) {
-        logger.warn(`transaction tip ${walletRewardId} not found`);
-        return;
-      }
-      let newStatus: RewardRecordStatus | undefined = undefined;
-      switch (tipRec.status) {
-        case RewardRecordStatus.Done:
-        case RewardRecordStatus.Aborted:
-        case RewardRecordStatus.PendingPickup:
-        case RewardRecordStatus.DialogAccept:
-          break;
-        case RewardRecordStatus.SuspendedPickup:
-          newStatus = RewardRecordStatus.Aborted;
-          break;
-        default:
-          assertUnreachable(tipRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computeRewardTransactionStatus(tipRec);
-        tipRec.status = newStatus;
-        const newTxState = computeRewardTransactionStatus(tipRec);
-        await tx.rewards.put(tipRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 142eff7c1..908aa540a 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -79,61 +79,45 @@ import {
   constructTaskIdentifier,
   resetPendingTaskTimeout,
   TaskIdentifiers,
-  TombstoneTag,
+  TransactionContext,
 } from "./common.js";
 import {
-  abortDepositGroup,
   computeDepositTransactionActions,
   computeDepositTransactionStatus,
-  deleteDepositGroup,
-  failDepositTransaction,
-  resumeDepositGroup,
-  suspendDepositGroup,
+  DepositTransactionContext,
 } from "./deposits.js";
 import {
   ExchangeWireDetails,
   getExchangeWireDetailsInTx,
 } from "./exchanges.js";
 import {
-  abortPayMerchant,
   computePayMerchantTransactionActions,
   computePayMerchantTransactionState,
   computeRefundTransactionState,
   expectProposalDownload,
   extractContractData,
-  failPaymentTransaction,
-  resumePayMerchant,
-  suspendPayMerchant,
+  PayMerchantTransactionContext,
+  RefundTransactionContext,
 } from "./pay-merchant.js";
 import {
-  abortPeerPullCreditTransaction,
   computePeerPullCreditTransactionActions,
   computePeerPullCreditTransactionState,
-  failPeerPullCreditTransaction,
-  resumePeerPullCreditTransaction,
-  suspendPeerPullCreditTransaction,
+  PeerPullCreditTransactionContext,
 } from "./pay-peer-pull-credit.js";
 import {
   computePeerPullDebitTransactionActions,
   computePeerPullDebitTransactionState,
   PeerPullDebitTransactionContext,
-  suspendPeerPullDebitTransaction,
 } from "./pay-peer-pull-debit.js";
 import {
-  abortPeerPushCreditTransaction,
   computePeerPushCreditTransactionActions,
   computePeerPushCreditTransactionState,
-  failPeerPushCreditTransaction,
-  resumePeerPushCreditTransaction,
-  suspendPeerPushCreditTransaction,
+  PeerPushCreditTransactionContext,
 } from "./pay-peer-push-credit.js";
 import {
-  abortPeerPushDebitTransaction,
   computePeerPushDebitTransactionActions,
   computePeerPushDebitTransactionState,
-  failPeerPushDebitTransaction,
-  resumePeerPushDebitTransaction,
-  suspendPeerPushDebitTransaction,
+  PeerPushDebitTransactionContext,
 } from "./pay-peer-push-debit.js";
 import {
   iterRecordsForDeposit,
@@ -148,29 +132,20 @@ import {
   iterRecordsForWithdrawal,
 } from "./pending.js";
 import {
-  abortRefreshGroup,
   computeRefreshTransactionActions,
   computeRefreshTransactionState,
-  failRefreshGroup,
-  resumeRefreshGroup,
-  suspendRefreshGroup,
+  RefreshTransactionContext,
 } from "./refresh.js";
 import {
-  abortTipTransaction,
   computeRewardTransactionStatus,
   computeTipTransactionActions,
-  failTipTransaction,
-  resumeTipTransaction,
-  suspendRewardTransaction,
+  RewardTransactionContext,
 } from "./reward.js";
 import {
-  abortWithdrawalTransaction,
   augmentPaytoUrisForWithdrawal,
   computeWithdrawalTransactionActions,
   computeWithdrawalTransactionStatus,
-  failWithdrawalTransaction,
-  resumeWithdrawalTransaction,
-  suspendWithdrawalTransaction,
+  WithdrawTransactionContext,
 } from "./withdraw.js";
 
 const logger = new Logger("taler-wallet-core:transactions.ts");
@@ -1565,100 +1540,61 @@ export async function retryTransaction(
   }
 }
 
-/**
- * Suspends a pending transaction, stopping any associated network activities,
- * but with a chance of trying again at a later time. This could be useful if
- * a user needs to save battery power or bandwidth and an operation is expected
- * to take longer (such as a backup, recovery or very large withdrawal 
operation).
- */
-export async function suspendTransaction(
+async function getContextForTransaction(
   ws: InternalWalletState,
   transactionId: string,
-): Promise<void> {
+): Promise<TransactionContext> {
   const tx = parseTransactionIdentifier(transactionId);
   if (!tx) {
     throw Error("invalid transaction ID");
   }
   switch (tx.tag) {
     case TransactionType.Deposit:
-      await suspendDepositGroup(ws, tx.depositGroupId);
-      return;
+      return new DepositTransactionContext(ws, tx.depositGroupId);
     case TransactionType.Refresh:
-      await suspendRefreshGroup(ws, tx.refreshGroupId);
-      return;
+      return new RefreshTransactionContext(ws, tx.refreshGroupId);
     case TransactionType.InternalWithdrawal:
     case TransactionType.Withdrawal:
-      await suspendWithdrawalTransaction(ws, tx.withdrawalGroupId);
-      return;
+      return new WithdrawTransactionContext(ws, tx.withdrawalGroupId);
     case TransactionType.Payment:
-      await suspendPayMerchant(ws, tx.proposalId);
-      return;
+      return new PayMerchantTransactionContext(ws, tx.proposalId);
     case TransactionType.PeerPullCredit:
-      await suspendPeerPullCreditTransaction(ws, tx.pursePub);
-      break;
+      return new PeerPullCreditTransactionContext(ws, tx.pursePub);
     case TransactionType.PeerPushDebit:
-      await suspendPeerPushDebitTransaction(ws, tx.pursePub);
-      break;
+      return new PeerPushDebitTransactionContext(ws, tx.pursePub);
     case TransactionType.PeerPullDebit:
-      await suspendPeerPullDebitTransaction(ws, tx.peerPullDebitId);
-      break;
+      return new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
     case TransactionType.PeerPushCredit:
-      await suspendPeerPushCreditTransaction(ws, tx.peerPushCreditId);
-      break;
+      return new PeerPushCreditTransactionContext(ws, tx.peerPushCreditId);
     case TransactionType.Refund:
-      throw Error("refund transactions can't be suspended or resumed");
+      return new RefundTransactionContext(ws, tx.refundGroupId);
     case TransactionType.Reward:
-      await suspendRewardTransaction(ws, tx.walletRewardId);
-      break;
+      return new RewardTransactionContext(ws, tx.walletRewardId);
     default:
       assertUnreachable(tx);
   }
 }
 
+/**
+ * Suspends a pending transaction, stopping any associated network activities,
+ * but with a chance of trying again at a later time. This could be useful if
+ * a user needs to save battery power or bandwidth and an operation is expected
+ * to take longer (such as a backup, recovery or very large withdrawal 
operation).
+ */
+export async function suspendTransaction(
+  ws: InternalWalletState,
+  transactionId: string,
+): Promise<void> {
+  const ctx = await getContextForTransaction(ws, transactionId);
+  await ctx.suspendTransaction();
+}
+
 export async function failTransaction(
   ws: InternalWalletState,
   transactionId: string,
 ): Promise<void> {
-  const tx = parseTransactionIdentifier(transactionId);
-  if (!tx) {
-    throw Error("invalid transaction ID");
-  }
-  switch (tx.tag) {
-    case TransactionType.Deposit:
-      await failDepositTransaction(ws, tx.depositGroupId);
-      return;
-    case TransactionType.InternalWithdrawal:
-    case TransactionType.Withdrawal:
-      await failWithdrawalTransaction(ws, tx.withdrawalGroupId);
-      return;
-    case TransactionType.Payment:
-      await failPaymentTransaction(ws, tx.proposalId);
-      return;
-    case TransactionType.Refund:
-      throw Error("can't do cancel-aborting on refund transaction");
-    case TransactionType.Reward:
-      await failTipTransaction(ws, tx.walletRewardId);
-      return;
-    case TransactionType.Refresh:
-      await failRefreshGroup(ws, tx.refreshGroupId);
-      return;
-    case TransactionType.PeerPullCredit:
-      await failPeerPullCreditTransaction(ws, tx.pursePub);
-      return;
-    case TransactionType.PeerPullDebit: {
-      const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
-      await ctx.failTransaction();
-      return;
-    }
-    case TransactionType.PeerPushCredit:
-      await failPeerPushCreditTransaction(ws, tx.peerPushCreditId);
-      return;
-    case TransactionType.PeerPushDebit:
-      await failPeerPushDebitTransaction(ws, tx.pursePub);
-      return;
-    default:
-      assertUnreachable(tx);
-  }
+  const ctx = await getContextForTransaction(ws, transactionId);
+  await ctx.failTransaction();
 }
 
 /**
@@ -1668,44 +1604,8 @@ export async function resumeTransaction(
   ws: InternalWalletState,
   transactionId: string,
 ): Promise<void> {
-  const tx = parseTransactionIdentifier(transactionId);
-  if (!tx) {
-    throw Error("invalid transaction ID");
-  }
-  switch (tx.tag) {
-    case TransactionType.Deposit:
-      await resumeDepositGroup(ws, tx.depositGroupId);
-      return;
-    case TransactionType.Refresh:
-      await resumeRefreshGroup(ws, tx.refreshGroupId);
-      return;
-    case TransactionType.InternalWithdrawal:
-    case TransactionType.Withdrawal:
-      await resumeWithdrawalTransaction(ws, tx.withdrawalGroupId);
-      return;
-    case TransactionType.Payment:
-      await resumePayMerchant(ws, tx.proposalId);
-      return;
-    case TransactionType.PeerPullCredit:
-      await resumePeerPullCreditTransaction(ws, tx.pursePub);
-      break;
-    case TransactionType.PeerPushDebit:
-      await resumePeerPushDebitTransaction(ws, tx.pursePub);
-      break;
-    case TransactionType.PeerPullDebit: {
-      const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
-      await ctx.resumeTransaction();
-      return;
-    }
-    case TransactionType.PeerPushCredit:
-      await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId);
-      break;
-    case TransactionType.Refund:
-      throw Error("refund transactions can't be suspended or resumed");
-    case TransactionType.Reward:
-      await resumeTipTransaction(ws, tx.walletRewardId);
-      break;
-  }
+  const ctx = await getContextForTransaction(ws, transactionId);
+  await ctx.resumeTransaction();
 }
 
 /**
@@ -1715,244 +1615,16 @@ export async function deleteTransaction(
   ws: InternalWalletState,
   transactionId: string,
 ): Promise<void> {
-  const parsedTx = parseTransactionIdentifier(transactionId);
-
-  if (!parsedTx) {
-    throw Error("invalid transaction ID");
-  }
-
-  switch (parsedTx.tag) {
-    case TransactionType.PeerPushCredit: {
-      const peerPushCreditId = parsedTx.peerPushCreditId;
-      await ws.db
-        .mktx((x) => [x.withdrawalGroups, x.peerPushCredit, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
-          if (!pushInc) {
-            return;
-          }
-          if (pushInc.withdrawalGroupId) {
-            const withdrawalGroupId = pushInc.withdrawalGroupId;
-            const withdrawalGroupRecord =
-              await tx.withdrawalGroups.get(withdrawalGroupId);
-            if (withdrawalGroupRecord) {
-              await tx.withdrawalGroups.delete(withdrawalGroupId);
-              await tx.tombstones.put({
-                id:
-                  TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
-              });
-            }
-          }
-          await tx.peerPushCredit.delete(peerPushCreditId);
-          await tx.tombstones.put({
-            id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
-          });
-        });
-      return;
-    }
-
-    case TransactionType.PeerPullCredit: {
-      const pursePub = parsedTx.pursePub;
-      await ws.db
-        .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const pullIni = await tx.peerPullCredit.get(pursePub);
-          if (!pullIni) {
-            return;
-          }
-          if (pullIni.withdrawalGroupId) {
-            const withdrawalGroupId = pullIni.withdrawalGroupId;
-            const withdrawalGroupRecord =
-              await tx.withdrawalGroups.get(withdrawalGroupId);
-            if (withdrawalGroupRecord) {
-              await tx.withdrawalGroups.delete(withdrawalGroupId);
-              await tx.tombstones.put({
-                id:
-                  TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
-              });
-            }
-          }
-          await tx.peerPullCredit.delete(pursePub);
-          await tx.tombstones.put({
-            id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
-          });
-        });
-
-      return;
-    }
-
-    case TransactionType.Withdrawal: {
-      const withdrawalGroupId = parsedTx.withdrawalGroupId;
-      await ws.db
-        .mktx((x) => [x.withdrawalGroups, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const withdrawalGroupRecord =
-            await tx.withdrawalGroups.get(withdrawalGroupId);
-          if (withdrawalGroupRecord) {
-            await tx.withdrawalGroups.delete(withdrawalGroupId);
-            await tx.tombstones.put({
-              id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
-            });
-            return;
-          }
-        });
-      return;
-    }
-
-    case TransactionType.Payment: {
-      const proposalId = parsedTx.proposalId;
-      await ws.db
-        .mktx((x) => [x.purchases, x.tombstones])
-        .runReadWrite(async (tx) => {
-          let found = false;
-          const purchase = await tx.purchases.get(proposalId);
-          if (purchase) {
-            found = true;
-            await tx.purchases.delete(proposalId);
-          }
-          if (found) {
-            await tx.tombstones.put({
-              id: TombstoneTag.DeletePayment + ":" + proposalId,
-            });
-          }
-        });
-      return;
-    }
-
-    case TransactionType.Refresh: {
-      const refreshGroupId = parsedTx.refreshGroupId;
-      await ws.db
-        .mktx((x) => [x.refreshGroups, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const rg = await tx.refreshGroups.get(refreshGroupId);
-          if (rg) {
-            await tx.refreshGroups.delete(refreshGroupId);
-            await tx.tombstones.put({
-              id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId,
-            });
-          }
-        });
-
-      return;
-    }
-
-    case TransactionType.Reward: {
-      const tipId = parsedTx.walletRewardId;
-      await ws.db
-        .mktx((x) => [x.rewards, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const tipRecord = await tx.rewards.get(tipId);
-          if (tipRecord) {
-            await tx.rewards.delete(tipId);
-            await tx.tombstones.put({
-              id: TombstoneTag.DeleteReward + ":" + tipId,
-            });
-          }
-        });
-      return;
-    }
-
-    case TransactionType.Deposit: {
-      const depositGroupId = parsedTx.depositGroupId;
-      await deleteDepositGroup(ws, depositGroupId);
-      return;
-    }
-
-    case TransactionType.Refund: {
-      const refundGroupId = parsedTx.refundGroupId;
-      await ws.db
-        .mktx((x) => [x.refundGroups, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const refundRecord = await tx.refundGroups.get(refundGroupId);
-          if (!refundRecord) {
-            return;
-          }
-          await tx.refundGroups.delete(refundGroupId);
-          await tx.tombstones.put({ id: transactionId });
-          // FIXME: Also tombstone the refund items, so that they won't 
reappear.
-        });
-      return;
-    }
-
-    case TransactionType.PeerPullDebit: {
-      const peerPullDebitId = parsedTx.peerPullDebitId;
-      await ws.db
-        .mktx((x) => [x.peerPullDebit, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const debit = await tx.peerPullDebit.get(peerPullDebitId);
-          if (debit) {
-            await tx.peerPullDebit.delete(peerPullDebitId);
-            await tx.tombstones.put({ id: transactionId });
-          }
-        });
-
-      return;
-    }
-
-    case TransactionType.PeerPushDebit: {
-      const pursePub = parsedTx.pursePub;
-      await ws.db
-        .mktx((x) => [x.peerPushDebit, x.tombstones])
-        .runReadWrite(async (tx) => {
-          const debit = await tx.peerPushDebit.get(pursePub);
-          if (debit) {
-            await tx.peerPushDebit.delete(pursePub);
-            await tx.tombstones.put({ id: transactionId });
-          }
-        });
-      return;
-    }
-  }
+  const ctx = await getContextForTransaction(ws, transactionId);
+  await ctx.deleteTransaction();
 }
 
 export async function abortTransaction(
   ws: InternalWalletState,
   transactionId: string,
 ): Promise<void> {
-  const txId = parseTransactionIdentifier(transactionId);
-  if (!txId) {
-    throw Error("invalid transaction identifier");
-  }
-
-  switch (txId.tag) {
-    case TransactionType.Payment: {
-      await abortPayMerchant(ws, txId.proposalId);
-      break;
-    }
-    case TransactionType.Withdrawal:
-    case TransactionType.InternalWithdrawal: {
-      await abortWithdrawalTransaction(ws, txId.withdrawalGroupId);
-      break;
-    }
-    case TransactionType.Deposit:
-      await abortDepositGroup(ws, txId.depositGroupId);
-      break;
-    case TransactionType.Reward:
-      await abortTipTransaction(ws, txId.walletRewardId);
-      break;
-    case TransactionType.Refund:
-      throw Error("can't abort refund transactions");
-    case TransactionType.Refresh:
-      await abortRefreshGroup(ws, txId.refreshGroupId);
-      break;
-    case TransactionType.PeerPullCredit:
-      await abortPeerPullCreditTransaction(ws, txId.pursePub);
-      break;
-    case TransactionType.PeerPullDebit: {
-      const ctx = new PeerPullDebitTransactionContext(ws, 
txId.peerPullDebitId);
-      await ctx.abortTransaction();
-      return;
-    }
-    case TransactionType.PeerPushCredit:
-      await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId);
-      break;
-    case TransactionType.PeerPushDebit:
-      await abortPeerPushDebitTransaction(ws, txId.pursePub);
-      break;
-    default: {
-      assertUnreachable(txId);
-    }
-  }
+  const ctx = await getContextForTransaction(ws, transactionId);
+  await ctx.abortTransaction();
 }
 
 export interface TransitionInfo {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index d02cf0597..58df75964 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -105,6 +105,8 @@ import {
   TaskIdentifiers,
   TaskRunResult,
   TaskRunResultType,
+  TombstoneTag,
+  TransactionContext,
   constructTaskIdentifier,
   makeCoinAvailable,
   makeCoinsVisible,
@@ -146,246 +148,246 @@ import {
  */
 const logger = new Logger("operations/withdraw.ts");
 
-export async function suspendWithdrawalTransaction(
-  ws: InternalWalletState,
-  withdrawalGroupId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.Withdraw,
-    withdrawalGroupId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.withdrawalGroups])
-    .runReadWrite(async (tx) => {
-      const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
-      if (!wg) {
-        logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
-        return;
-      }
-      let newStatus: WithdrawalGroupStatus | undefined = undefined;
-      switch (wg.status) {
-        case WithdrawalGroupStatus.PendingReady:
-          newStatus = WithdrawalGroupStatus.SuspendedReady;
-          break;
-        case WithdrawalGroupStatus.AbortingBank:
-          newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
-          break;
-        case WithdrawalGroupStatus.PendingWaitConfirmBank:
-          newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
-          break;
-        case WithdrawalGroupStatus.PendingRegisteringBank:
-          newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
-          break;
-        case WithdrawalGroupStatus.PendingQueryingStatus:
-          newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
-          break;
-        case WithdrawalGroupStatus.PendingKyc:
-          newStatus = WithdrawalGroupStatus.SuspendedKyc;
-          break;
-        case WithdrawalGroupStatus.PendingAml:
-          newStatus = WithdrawalGroupStatus.SuspendedAml;
-          break;
-        default:
-          logger.warn(
-            `Unsupported 'suspend' on withdrawal transaction in status 
${wg.status}`,
-          );
-      }
-      if (newStatus != null) {
-        const oldTxState = computeWithdrawalTransactionStatus(wg);
-        wg.status = newStatus;
-        const newTxState = computeWithdrawalTransactionStatus(wg);
-        await tx.withdrawalGroups.put(wg);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
+export class WithdrawTransactionContext implements TransactionContext {
+  public transactionId: string;
+  public retryTag: string;
+
+  constructor(
+    public ws: InternalWalletState,
+    public withdrawalGroupId: string,
+  ) {
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.Withdrawal,
+      withdrawalGroupId,
     });
+    this.retryTag = constructTaskIdentifier({
+      tag: PendingTaskType.Withdraw,
+      withdrawalGroupId,
+    });
+  }
 
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Withdrawal,
-    withdrawalGroupId,
-  });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
+  async deleteTransaction(): Promise<void> {
+    const { ws, withdrawalGroupId } = this;
+    await ws.db
+      .mktx((x) => [x.withdrawalGroups, x.tombstones])
+      .runReadWrite(async (tx) => {
+        const withdrawalGroupRecord =
+          await tx.withdrawalGroups.get(withdrawalGroupId);
+        if (withdrawalGroupRecord) {
+          await tx.withdrawalGroups.delete(withdrawalGroupId);
+          await tx.tombstones.put({
+            id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
+          });
+          return;
+        }
+      });
+  }
 
-export async function resumeWithdrawalTransaction(
-  ws: InternalWalletState,
-  withdrawalGroupId: string,
-) {
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.withdrawalGroups])
-    .runReadWrite(async (tx) => {
-      const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
-      if (!wg) {
-        logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
-        return;
-      }
-      let newStatus: WithdrawalGroupStatus | undefined = undefined;
-      switch (wg.status) {
-        case WithdrawalGroupStatus.SuspendedReady:
-          newStatus = WithdrawalGroupStatus.PendingReady;
-          break;
-        case WithdrawalGroupStatus.SuspendedAbortingBank:
-          newStatus = WithdrawalGroupStatus.AbortingBank;
-          break;
-        case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
-          newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
-          break;
-        case WithdrawalGroupStatus.SuspendedQueryingStatus:
-          newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
-          break;
-        case WithdrawalGroupStatus.SuspendedRegisteringBank:
-          newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
-          break;
-        case WithdrawalGroupStatus.SuspendedAml:
-          newStatus = WithdrawalGroupStatus.PendingAml;
-          break;
-        case WithdrawalGroupStatus.SuspendedKyc:
-          newStatus = WithdrawalGroupStatus.PendingKyc;
-          break;
-        default:
-          logger.warn(
-            `Unsupported 'resume' on withdrawal transaction in status 
${wg.status}`,
-          );
-      }
-      if (newStatus != null) {
-        const oldTxState = computeWithdrawalTransactionStatus(wg);
-        wg.status = newStatus;
-        const newTxState = computeWithdrawalTransactionStatus(wg);
-        await tx.withdrawalGroups.put(wg);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Withdrawal,
-    withdrawalGroupId,
-  });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
+  async suspendTransaction(): Promise<void> {
+    const { ws, withdrawalGroupId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.withdrawalGroups])
+      .runReadWrite(async (tx) => {
+        const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+        if (!wg) {
+          logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+          return;
+        }
+        let newStatus: WithdrawalGroupStatus | undefined = undefined;
+        switch (wg.status) {
+          case WithdrawalGroupStatus.PendingReady:
+            newStatus = WithdrawalGroupStatus.SuspendedReady;
+            break;
+          case WithdrawalGroupStatus.AbortingBank:
+            newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
+            break;
+          case WithdrawalGroupStatus.PendingWaitConfirmBank:
+            newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
+            break;
+          case WithdrawalGroupStatus.PendingRegisteringBank:
+            newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
+            break;
+          case WithdrawalGroupStatus.PendingQueryingStatus:
+            newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
+            break;
+          case WithdrawalGroupStatus.PendingKyc:
+            newStatus = WithdrawalGroupStatus.SuspendedKyc;
+            break;
+          case WithdrawalGroupStatus.PendingAml:
+            newStatus = WithdrawalGroupStatus.SuspendedAml;
+            break;
+          default:
+            logger.warn(
+              `Unsupported 'suspend' on withdrawal transaction in status 
${wg.status}`,
+            );
+        }
+        if (newStatus != null) {
+          const oldTxState = computeWithdrawalTransactionStatus(wg);
+          wg.status = newStatus;
+          const newTxState = computeWithdrawalTransactionStatus(wg);
+          await tx.withdrawalGroups.put(wg);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
 
-export async function abortWithdrawalTransaction(
-  ws: InternalWalletState,
-  withdrawalGroupId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.Withdraw,
-    withdrawalGroupId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Withdrawal,
-    withdrawalGroupId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.withdrawalGroups])
-    .runReadWrite(async (tx) => {
-      const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
-      if (!wg) {
-        logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
-        return;
-      }
-      let newStatus: WithdrawalGroupStatus | undefined = undefined;
-      switch (wg.status) {
-        case WithdrawalGroupStatus.SuspendedRegisteringBank:
-        case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
-        case WithdrawalGroupStatus.PendingWaitConfirmBank:
-        case WithdrawalGroupStatus.PendingRegisteringBank:
-          newStatus = WithdrawalGroupStatus.AbortingBank;
-          break;
-        case WithdrawalGroupStatus.SuspendedAml:
-        case WithdrawalGroupStatus.SuspendedKyc:
-        case WithdrawalGroupStatus.SuspendedQueryingStatus:
-        case WithdrawalGroupStatus.SuspendedReady:
-        case WithdrawalGroupStatus.PendingAml:
-        case WithdrawalGroupStatus.PendingKyc:
-        case WithdrawalGroupStatus.PendingQueryingStatus:
-          newStatus = WithdrawalGroupStatus.AbortedExchange;
-          break;
-        case WithdrawalGroupStatus.PendingReady:
-          newStatus = WithdrawalGroupStatus.SuspendedReady;
-          break;
-        case WithdrawalGroupStatus.SuspendedAbortingBank:
-        case WithdrawalGroupStatus.AbortingBank:
-          // No transition needed, but not an error
-          break;
-        case WithdrawalGroupStatus.Done:
-        case WithdrawalGroupStatus.FailedBankAborted:
-        case WithdrawalGroupStatus.AbortedExchange:
-        case WithdrawalGroupStatus.AbortedBank:
-        case WithdrawalGroupStatus.FailedAbortingBank:
-          // Not allowed
-          throw Error("abort not allowed in current state");
-          break;
-        default:
-          assertUnreachable(wg.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computeWithdrawalTransactionStatus(wg);
-        wg.status = newStatus;
-        const newTxState = computeWithdrawalTransactionStatus(wg);
-        await tx.withdrawalGroups.put(wg);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
 
-export async function failWithdrawalTransaction(
-  ws: InternalWalletState,
-  withdrawalGroupId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.Withdraw,
-    withdrawalGroupId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.Withdrawal,
-    withdrawalGroupId,
-  });
-  stopLongpolling(ws, taskId);
-  const stateUpdate = await ws.db
-    .mktx((x) => [x.withdrawalGroups])
-    .runReadWrite(async (tx) => {
-      const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
-      if (!wg) {
-        logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
-        return;
-      }
-      let newStatus: WithdrawalGroupStatus | undefined = undefined;
-      switch (wg.status) {
-        case WithdrawalGroupStatus.SuspendedAbortingBank:
-        case WithdrawalGroupStatus.AbortingBank:
-          newStatus = WithdrawalGroupStatus.FailedAbortingBank;
-          break;
-        default:
-          break;
-      }
-      if (newStatus != null) {
-        const oldTxState = computeWithdrawalTransactionStatus(wg);
-        wg.status = newStatus;
-        const newTxState = computeWithdrawalTransactionStatus(wg);
-        await tx.withdrawalGroups.put(wg);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, stateUpdate);
+  async abortTransaction(): Promise<void> {
+    const { ws, withdrawalGroupId, transactionId } = this;
+    stopLongpolling(ws, this.retryTag);
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.withdrawalGroups])
+      .runReadWrite(async (tx) => {
+        const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+        if (!wg) {
+          logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+          return;
+        }
+        let newStatus: WithdrawalGroupStatus | undefined = undefined;
+        switch (wg.status) {
+          case WithdrawalGroupStatus.SuspendedRegisteringBank:
+          case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+          case WithdrawalGroupStatus.PendingWaitConfirmBank:
+          case WithdrawalGroupStatus.PendingRegisteringBank:
+            newStatus = WithdrawalGroupStatus.AbortingBank;
+            break;
+          case WithdrawalGroupStatus.SuspendedAml:
+          case WithdrawalGroupStatus.SuspendedKyc:
+          case WithdrawalGroupStatus.SuspendedQueryingStatus:
+          case WithdrawalGroupStatus.SuspendedReady:
+          case WithdrawalGroupStatus.PendingAml:
+          case WithdrawalGroupStatus.PendingKyc:
+          case WithdrawalGroupStatus.PendingQueryingStatus:
+            newStatus = WithdrawalGroupStatus.AbortedExchange;
+            break;
+          case WithdrawalGroupStatus.PendingReady:
+            newStatus = WithdrawalGroupStatus.SuspendedReady;
+            break;
+          case WithdrawalGroupStatus.SuspendedAbortingBank:
+          case WithdrawalGroupStatus.AbortingBank:
+            // No transition needed, but not an error
+            break;
+          case WithdrawalGroupStatus.Done:
+          case WithdrawalGroupStatus.FailedBankAborted:
+          case WithdrawalGroupStatus.AbortedExchange:
+          case WithdrawalGroupStatus.AbortedBank:
+          case WithdrawalGroupStatus.FailedAbortingBank:
+            // Not allowed
+            throw Error("abort not allowed in current state");
+            break;
+          default:
+            assertUnreachable(wg.status);
+        }
+        if (newStatus != null) {
+          const oldTxState = computeWithdrawalTransactionStatus(wg);
+          wg.status = newStatus;
+          const newTxState = computeWithdrawalTransactionStatus(wg);
+          await tx.withdrawalGroups.put(wg);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const { ws, withdrawalGroupId, transactionId } = this;
+    const transitionInfo = await ws.db
+      .mktx((x) => [x.withdrawalGroups])
+      .runReadWrite(async (tx) => {
+        const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+        if (!wg) {
+          logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+          return;
+        }
+        let newStatus: WithdrawalGroupStatus | undefined = undefined;
+        switch (wg.status) {
+          case WithdrawalGroupStatus.SuspendedReady:
+            newStatus = WithdrawalGroupStatus.PendingReady;
+            break;
+          case WithdrawalGroupStatus.SuspendedAbortingBank:
+            newStatus = WithdrawalGroupStatus.AbortingBank;
+            break;
+          case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+            newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank;
+            break;
+          case WithdrawalGroupStatus.SuspendedQueryingStatus:
+            newStatus = WithdrawalGroupStatus.PendingQueryingStatus;
+            break;
+          case WithdrawalGroupStatus.SuspendedRegisteringBank:
+            newStatus = WithdrawalGroupStatus.PendingRegisteringBank;
+            break;
+          case WithdrawalGroupStatus.SuspendedAml:
+            newStatus = WithdrawalGroupStatus.PendingAml;
+            break;
+          case WithdrawalGroupStatus.SuspendedKyc:
+            newStatus = WithdrawalGroupStatus.PendingKyc;
+            break;
+          default:
+            logger.warn(
+              `Unsupported 'resume' on withdrawal transaction in status 
${wg.status}`,
+            );
+        }
+        if (newStatus != null) {
+          const oldTxState = computeWithdrawalTransactionStatus(wg);
+          wg.status = newStatus;
+          const newTxState = computeWithdrawalTransactionStatus(wg);
+          await tx.withdrawalGroups.put(wg);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    ws.workAvailable.trigger();
+    notifyTransition(ws, transactionId, transitionInfo);
+  }
+
+  async failTransaction(): Promise<void> {
+    const { ws, withdrawalGroupId, transactionId, retryTag } = this;
+    stopLongpolling(ws, retryTag);
+    const stateUpdate = await ws.db
+      .mktx((x) => [x.withdrawalGroups])
+      .runReadWrite(async (tx) => {
+        const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
+        if (!wg) {
+          logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
+          return;
+        }
+        let newStatus: WithdrawalGroupStatus | undefined = undefined;
+        switch (wg.status) {
+          case WithdrawalGroupStatus.SuspendedAbortingBank:
+          case WithdrawalGroupStatus.AbortingBank:
+            newStatus = WithdrawalGroupStatus.FailedAbortingBank;
+            break;
+          default:
+            break;
+        }
+        if (newStatus != null) {
+          const oldTxState = computeWithdrawalTransactionStatus(wg);
+          wg.status = newStatus;
+          const newTxState = computeWithdrawalTransactionStatus(wg);
+          await tx.withdrawalGroups.put(wg);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        return undefined;
+      });
+    notifyTransition(ws, transactionId, stateUpdate);
+  }
 }
 
 export function computeWithdrawalTransactionStatus(
@@ -2413,6 +2415,16 @@ export async function 
internalPerformCreateWithdrawalGroup(
       exchangeNotif: undefined,
     };
   }
+  const existingWg = await tx.withdrawalGroups.get(
+    withdrawalGroup.withdrawalGroupId,
+  );
+  if (existingWg) {
+    return {
+      withdrawalGroup: existingWg,
+      exchangeNotif: undefined,
+      transitionInfo: undefined,
+    };
+  }
   await tx.withdrawalGroups.add(withdrawalGroup);
   await tx.reserves.put({
     reservePub: withdrawalGroup.reservePub,
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 5d563f620..17b9b407c 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -581,7 +581,7 @@ export type DbReadOnlyTransactionArr<
     }
   : never;
 
-export interface TransactionContext<BoundStores> {
+export interface DbTransactionContext<BoundStores> {
   runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
   runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
 }
@@ -804,7 +804,7 @@ export class DbAccess<StoreMap> {
   /**
    * Run a transaction with all object stores.
    */
-  mktxAll(): TransactionContext<StoreMap> {
+  mktxAll(): DbTransactionContext<StoreMap> {
     const storeNames: string[] = [];
     const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
       {};
@@ -904,7 +904,7 @@ export class DbAccess<StoreMap> {
     BoundStores extends {
       [X in StoreNamesOf<StoreList>]: StoreList[number] & { storeName: X };
     },
-  >(namePicker: (x: StoreMap) => StoreList): TransactionContext<BoundStores> {
+  >(namePicker: (x: StoreMap) => StoreList): DbTransactionContext<BoundStores> 
{
     const storeNames: string[] = [];
     const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
       {};
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 1fa1d117e..d6da2250a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -151,7 +151,6 @@ import {
   CancelFn,
   InternalWalletState,
   MerchantInfo,
-  MerchantOperations,
   NotificationListener,
   RecoupOperations,
   RefreshOperations,
@@ -246,7 +245,7 @@ import {
 import {
   acceptTip,
   computeRewardTransactionStatus,
-  prepareTip,
+  prepareReward,
   processTip,
 } from "./operations/reward.js";
 import {
@@ -1164,7 +1163,7 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
     }
     case WalletApiOperation.PrepareReward: {
       const req = codecForPrepareRewardRequest().decode(payload);
-      return await prepareTip(ws, req.talerRewardUri);
+      return await prepareReward(ws, req.talerRewardUri);
     }
     case WalletApiOperation.StartRefundQueryForUri: {
       const req = codecForPrepareRefundRequest().decode(payload);
@@ -1609,9 +1608,6 @@ class InternalWalletStateImpl implements 
InternalWalletState {
     createRecoupGroup,
   };
 
-  merchantOps: MerchantOperations = {
-    getMerchantInfo,
-  };
 
   refreshOps: RefreshOperations = {
     createRefreshGroup,

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