gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (eace0e0e7 -> 8ac508060)


From: gnunet
Subject: [taler-wallet-core] branch master updated (eace0e0e7 -> 8ac508060)
Date: Sat, 08 Oct 2022 23:45:53 +0200

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

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

    from eace0e0e7 -organize imports
     new 526f4eba9 wallet-core: Clean up merchant payments DB schema
     new 3897bd4f0 wallet-core: spec/clarify explicit refresh transaction
     new 8ac508060 wallet-core: more DB and refund fixes

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .vscode/settings.json                              |  104 +-
 packages/idb-bridge/src/MemoryBackend.ts           |   26 +-
 packages/taler-util/src/backupTypes.ts             |  166 ++-
 packages/taler-util/src/transactionsTypes.ts       |   30 +-
 .../src/integrationtests/test-denom-unoffered.ts   |    7 +-
 .../integrationtests/test-payment-idempotency.ts   |   29 +-
 packages/taler-wallet-core/src/bank-api-client.ts  |    6 +-
 packages/taler-wallet-core/src/db.ts               |  279 +++--
 packages/taler-wallet-core/src/dbless.ts           |   28 +-
 packages/taler-wallet-core/src/index.ts            |    1 -
 .../taler-wallet-core/src/internal-wallet-state.ts |   18 +
 .../src/operations/backup/export.ts                |   99 +-
 .../src/operations/backup/import.ts                |  203 +---
 .../src/operations/backup/index.ts                 |   11 +-
 .../taler-wallet-core/src/operations/common.ts     |  292 ++++-
 .../taler-wallet-core/src/operations/deposits.ts   |    5 +-
 .../taler-wallet-core/src/operations/exchanges.ts  |    3 -
 .../taler-wallet-core/src/operations/merchants.ts  |    4 +-
 .../src/operations/{pay.ts => pay-merchant.ts}     | 1230 +++++++++++++++++---
 .../operations/{peer-to-peer.ts => pay-peer.ts}    |    3 +-
 .../taler-wallet-core/src/operations/pending.ts    |   88 +-
 .../taler-wallet-core/src/operations/recoup.ts     |    7 +-
 .../taler-wallet-core/src/operations/refresh.ts    |    2 +-
 .../taler-wallet-core/src/operations/refund.ts     |  815 -------------
 .../taler-wallet-core/src/operations/testing.ts    |    5 +-
 packages/taler-wallet-core/src/operations/tip.ts   |    5 +-
 .../src/operations/transactions.ts                 |  147 +--
 .../taler-wallet-core/src/operations/withdraw.ts   |   14 +-
 packages/taler-wallet-core/src/pending-types.ts    |   44 +-
 packages/taler-wallet-core/src/util/retries.ts     |   11 +-
 packages/taler-wallet-core/src/wallet.ts           |  273 +----
 31 files changed, 1864 insertions(+), 2091 deletions(-)
 rename packages/taler-wallet-core/src/operations/{pay.ts => pay-merchant.ts} 
(60%)
 rename packages/taler-wallet-core/src/operations/{peer-to-peer.ts => 
pay-peer.ts} (99%)
 delete mode 100644 packages/taler-wallet-core/src/operations/refund.ts

diff --git a/.vscode/settings.json b/.vscode/settings.json
index d8e616936..97596d26c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,53 +1,59 @@
 // Place your settings in this file to overwrite default and user settings.
 {
-    // Use latest language servicesu
-    "typescript.tsdk": "./node_modules/typescript/lib",
-    // Defines space handling after a comma delimiter
-    "typescript.format.insertSpaceAfterCommaDelimiter": true,
-    //  Defines space handling after a semicolon in a for statement
-    "typescript.format.insertSpaceAfterSemicolonInForStatements": true,
-    // Defines space handling after a binary operator
-    "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true,
-    // Defines space handling after keywords in control flow statement
-    "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true,
-    // Defines space handling after function keyword for anonymous functions
-    "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": 
true,
-    // Defines space handling after opening and before closing non empty 
parenthesis
-    
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": 
false,
-    // Defines space handling after opening and before closing non empty 
brackets
-    
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": 
false,
-    // Defines whether an open brace is put onto a new line for functions or 
not
-    "typescript.format.placeOpenBraceOnNewLineForFunctions": false,
-    // Defines whether an open brace is put onto a new line for control blocks 
or not
-    "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false,
-    // Files hidden in the explorer
-    "files.exclude": {
-        // include the defaults from VS Code
-        "**/.git": true,
-        "**/.DS_Store": true,
-        // exclude .js and .js.map files, when in a TypeScript project
-        "**/*.js": {
-            "when": "$(basename).ts"
-        },
-        "**/*?.js": {
-            "when": "$(basename).tsx"
-        },
-        "**/*.js.map": true
+  // Use latest language servicesu
+  "typescript.tsdk": "./node_modules/typescript/lib",
+  // Defines space handling after a comma delimiter
+  "typescript.format.insertSpaceAfterCommaDelimiter": true,
+  //  Defines space handling after a semicolon in a for statement
+  "typescript.format.insertSpaceAfterSemicolonInForStatements": true,
+  // Defines space handling after a binary operator
+  "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true,
+  // Defines space handling after keywords in control flow statement
+  "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true,
+  // Defines space handling after function keyword for anonymous functions
+  "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": 
true,
+  // Defines space handling after opening and before closing non empty 
parenthesis
+  
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": 
false,
+  // Defines space handling after opening and before closing non empty brackets
+  "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": 
false,
+  // Defines whether an open brace is put onto a new line for functions or not
+  "typescript.format.placeOpenBraceOnNewLineForFunctions": false,
+  // Defines whether an open brace is put onto a new line for control blocks 
or not
+  "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false,
+  "typescript.preferences.autoImportFileExcludePatterns": [
+    "index.js",
+    "index.*.js",
+    "index.ts",
+    "index.*.ts"
+  ],
+  // Files hidden in the explorer
+  "files.exclude": {
+    // include the defaults from VS Code
+    "**/.git": true,
+    "**/.DS_Store": true,
+    // exclude .js and .js.map files, when in a TypeScript project
+    "**/*.js": {
+      "when": "$(basename).ts"
     },
-    "editor.wrappingIndent": "same",
-    "editor.tabSize": 2,
-    "search.exclude": {
-        "dist": true,
-        "prebuilt": true,
-        "src/i18n/*.po": true,
-        "vendor": true
+    "**/*?.js": {
+      "when": "$(basename).tsx"
     },
-    "search.collapseResults": "auto",
-    "files.associations": {
-        "api-extractor.json": "jsonc"
-    },
-    "typescript.preferences.importModuleSpecifierEnding": "js",
-    "typescript.preferences.importModuleSpecifier": "project-relative",
-    "javascript.preferences.importModuleSpecifier": "project-relative",
-    "javascript.preferences.importModuleSpecifierEnding": "js"
-}
\ No newline at end of file
+    "**/*.js.map": true
+  },
+  "editor.wrappingIndent": "same",
+  "editor.tabSize": 2,
+  "search.exclude": {
+    "dist": true,
+    "prebuilt": true,
+    "src/i18n/*.po": true,
+    "vendor": true
+  },
+  "search.collapseResults": "auto",
+  "files.associations": {
+    "api-extractor.json": "jsonc"
+  },
+  "typescript.preferences.importModuleSpecifierEnding": "js",
+  "typescript.preferences.importModuleSpecifier": "project-relative",
+  "javascript.preferences.importModuleSpecifier": "project-relative",
+  "javascript.preferences.importModuleSpecifierEnding": "js"
+}
diff --git a/packages/idb-bridge/src/MemoryBackend.ts 
b/packages/idb-bridge/src/MemoryBackend.ts
index 3919cdf97..f40f1c98b 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -378,9 +378,9 @@ export class MemoryBackend implements Backend {
     }
   }
 
-  private makeObjectStoreMap(
-    database: Database,
-  ): { [currentName: string]: ObjectStoreMapEntry } {
+  private makeObjectStoreMap(database: Database): {
+    [currentName: string]: ObjectStoreMapEntry;
+  } {
     let map: { [currentName: string]: ObjectStoreMapEntry } = {};
     for (let objectStoreName in database.committedObjectStores) {
       const store = database.committedObjectStores[objectStoreName];
@@ -1088,9 +1088,8 @@ export class MemoryBackend implements Backend {
       if (!existingIndexRecord) {
         throw Error("db inconsistent: expected index entry missing");
       }
-      const newPrimaryKeys = existingIndexRecord.primaryKeys.without(
-        primaryKey,
-      );
+      const newPrimaryKeys =
+        existingIndexRecord.primaryKeys.without(primaryKey);
       if (newPrimaryKeys.size === 0) {
         index.modifiedData = indexData.without(indexKey);
       } else {
@@ -1357,7 +1356,20 @@ export class MemoryBackend implements Backend {
 
       // Remove old index entry first!
       if (oldStoreRecord) {
-        this.deleteFromIndex(index, key, oldStoreRecord.value, 
indexProperties);
+        try {
+          this.deleteFromIndex(
+            index,
+            key,
+            oldStoreRecord.value,
+            indexProperties,
+          );
+        } catch (e) {
+          if (e instanceof DataError) {
+            // Do nothing
+          } else {
+            throw e;
+          }
+        }
       }
       try {
         this.insertIntoIndex(index, key, value, indexProperties);
diff --git a/packages/taler-util/src/backupTypes.ts 
b/packages/taler-util/src/backupTypes.ts
index 19d478178..777086599 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -180,15 +180,6 @@ export interface WalletBackupContentV1 {
    */
   tips: BackupTip[];
 
-  /**
-   * Proposals from merchants.  The proposal may
-   * be deleted as soon as it has been accepted (and thus
-   * turned into a purchase).
-   *
-   * Sorted by the proposal ID.
-   */
-  proposals: BackupProposal[];
-
   /**
    * Accepted purchases.
    *
@@ -838,29 +829,10 @@ export type BackupRefundItem =
   | BackupRefundPendingItem
   | BackupRefundAppliedItem;
 
-export interface BackupPurchase {
-  /**
-   * Proposal ID for this purchase.  Uniquely identifies the
-   * purchase and the proposal.
-   */
-  proposal_id: string;
-
-  /**
-   * Contract terms we got from the merchant.
-   */
-  contract_terms_raw: RawContractTerms;
-
-  /**
-   * Signature on the contract terms.
-   */
-  merchant_sig: string;
-
-  /**
-   * Private key for the nonce.  Might eventually be used
-   * to prove ownership of the contract.
-   */
-  nonce_priv: string;
-
+/**
+ * Data we store when the payment was accepted.
+ */
+export interface BackupPayInfo {
   pay_coins: {
     /**
      * Public keys of the coins that were selected.
@@ -890,6 +862,63 @@ export interface BackupPurchase {
    * We might show adjustments to this later, but currently we don't do so.
    */
   total_pay_cost: BackupAmountString;
+}
+
+export interface BackupPurchase {
+  /**
+   * Proposal ID for this purchase.  Uniquely identifies the
+   * purchase and the proposal.
+   */
+  proposal_id: string;
+
+  /**
+   * Status of the proposal.
+   */
+  proposal_status: BackupProposalStatus;
+
+  /**
+   * Proposal that this one got "redirected" to as part of
+   * the repurchase detection.
+   */
+  repurchase_proposal_id: string | undefined;
+
+  /**
+   * Session ID we got when downloading the contract.
+   */
+  download_session_id?: string;
+
+  /**
+   * Merchant-assigned order ID of the proposal.
+   */
+  order_id: string;
+
+  /**
+   * Base URL of the merchant that proposed the purchase.
+   */
+  merchant_base_url: string;
+
+  /**
+   * Claim token initially given by the merchant.
+   */
+  claim_token: string | undefined;
+
+  /**
+   * Contract terms we got from the merchant.
+   */
+  contract_terms_raw?: RawContractTerms;
+
+  /**
+   * Signature on the contract terms.
+   */
+  merchant_sig?: string;
+
+  /**
+   * Private key for the nonce.  Might eventually be used
+   * to prove ownership of the contract.
+   */
+  nonce_priv: string;
+
+  pay_info: BackupPayInfo | undefined;
 
   /**
    * Timestamp of the first time that sending a payment to the merchant
@@ -902,11 +931,13 @@ export interface BackupPurchase {
    */
   merchant_pay_sig: string | undefined;
 
+  timestamp_proposed: TalerProtocolTimestamp;
+
   /**
    * When was the purchase made?
    * Refers to the time that the user accepted.
    */
-  timestamp_accept: TalerProtocolTimestamp;
+  timestamp_accepted: TalerProtocolTimestamp | undefined;
 
   /**
    * Pending refunds for the purchase.  A refund is pending
@@ -914,11 +945,6 @@ export interface BackupPurchase {
    */
   refunds: BackupRefundItem[];
 
-  /**
-   * Abort status of the payment.
-   */
-  abort_status?: "abort-refund" | "abort-finished";
-
   /**
    * Continue querying the refund status until this deadline has expired.
    */
@@ -1218,70 +1244,8 @@ export enum BackupProposalStatus {
    * Downloaded proposal was detected as a re-purchase.
    */
   Repurchase = "repurchase",
-}
-
-/**
- * Proposal by a merchant.
- */
-export interface BackupProposal {
-  /**
-   * Base URL of the merchant that proposed the purchase.
-   */
-  merchant_base_url: string;
-
-  /**
-   * Downloaded data from the merchant.
-   */
-  contract_terms_raw?: RawContractTerms;
-
-  /**
-   * Signature on the contract terms.
-   *
-   * Must be present if contract_terms_raw is present.
-   */
-  merchant_sig?: string;
-
-  /**
-   * Unique ID when the order is stored in the wallet DB.
-   */
-  proposal_id: string;
-
-  /**
-   * Merchant-assigned order ID of the proposal.
-   */
-  order_id: string;
-
-  /**
-   * Timestamp of when the record
-   * was created.
-   */
-  timestamp: TalerProtocolTimestamp;
-
-  /**
-   * Private key for the nonce.
-   */
-  nonce_priv: string;
-
-  /**
-   * Claim token initially given by the merchant.
-   */
-  claim_token: string | undefined;
-
-  /**
-   * Status of the proposal.
-   */
-  proposal_status: BackupProposalStatus;
-
-  /**
-   * Proposal that this one got "redirected" to as part of
-   * the repurchase detection.
-   */
-  repurchase_proposal_id: string | undefined;
 
-  /**
-   * Session ID we got when downloading the contract.
-   */
-  download_session_id?: string;
+  Paid = "paid",
 }
 
 export interface BackupRecovery {
diff --git a/packages/taler-util/src/transactionsTypes.ts 
b/packages/taler-util/src/transactionsTypes.ts
index 3dc4a93d7..5fd01448c 100644
--- a/packages/taler-util/src/transactionsTypes.ts
+++ b/packages/taler-util/src/transactionsTypes.ts
@@ -43,7 +43,7 @@ import {
   codecForList,
   codecForAny,
 } from "./codec.js";
-import { TalerErrorDetail } from "./walletTypes.js";
+import { RefreshReason, TalerErrorDetail } from "./walletTypes.js";
 
 export interface TransactionsRequest {
   /**
@@ -468,19 +468,35 @@ export interface TransactionTip extends TransactionCommon 
{
   merchantBaseUrl: string;
 }
 
-// A transaction shown for refreshes that are not associated to other 
transactions
-// such as a refresh necessary before coin expiration.
-// It should only be returned by the API if the effective amount is different 
from zero.
+/**
+ * A transaction shown for refreshes.
+ * Only shown for (1) refreshes not associated with other transactions
+ * and (2) refreshes in an error state.
+ */
 export interface TransactionRefresh extends TransactionCommon {
   type: TransactionType.Refresh;
 
-  // Exchange that the coins are refreshed with
+  /**
+   * Exchange that the coins are refreshed with
+   */
   exchangeBaseUrl: string;
 
-  // Raw amount that is refreshed
+  refreshReason: RefreshReason;
+
+  /**
+   * Transaction ID that caused this refresh.
+   */
+  originatingTransactionId?: string;
+
+  /**
+   * Always zero for refreshes
+   */
   amountRaw: AmountString;
 
-  // Amount that will be paid as fees for the refresh
+  /**
+   * Fees, i.e. the effective, negative effect of the refresh
+   * on the balance.
+   */
   amountEffective: AmountString;
 }
 
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
index ec1d9f64b..b5ecbee4a 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts
@@ -17,13 +17,8 @@
 /**
  * Imports.
  */
-import {
-  PreparePayResultType,
-  TalerErrorCode,
-  TalerErrorDetail,
-} from "@gnu-taler/taler-util";
+import { PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { makeEventId } from "@gnu-taler/taler-wallet-core";
 import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
 import {
   createSimpleTestkudosEnvironment,
diff --git 
a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts 
b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts
index 9378465a0..1099a8188 100644
--- a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts
@@ -18,7 +18,10 @@
  * Imports.
  */
 import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from 
"../harness/helpers.js";
+import {
+  createSimpleTestkudosEnvironment,
+  withdrawViaBank,
+} from "../harness/helpers.js";
 import { PreparePayResultType } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 
@@ -29,12 +32,8 @@ import { WalletApiOperation } from 
"@gnu-taler/taler-wallet-core";
 export async function runPaymentIdempotencyTest(t: GlobalTestState) {
   // Set up test environment
 
-  const {
-    wallet,
-    bank,
-    exchange,
-    merchant,
-  } = await createSimpleTestkudosEnvironment(t);
+  const { wallet, bank, exchange, merchant } =
+    await createSimpleTestkudosEnvironment(t);
 
   // Withdraw digital cash into the wallet.
 
@@ -83,10 +82,16 @@ export async function runPaymentIdempotencyTest(t: 
GlobalTestState) {
 
   const proposalId = preparePayResult.proposalId;
 
-  await wallet.client.call(WalletApiOperation.ConfirmPay, {
-    // FIXME: should be validated, don't cast!
-    proposalId: proposalId,
-  });
+  const confirmPayResult = await wallet.client.call(
+    WalletApiOperation.ConfirmPay,
+    {
+      proposalId: proposalId,
+    },
+  );
+
+  console.log("confirm pay result", confirmPayResult);
+
+  await wallet.runUntilDone();
 
   // Check if payment was successful.
 
@@ -103,6 +108,8 @@ export async function runPaymentIdempotencyTest(t: 
GlobalTestState) {
     },
   );
 
+  console.log("result after:", preparePayResultAfter);
+
   t.assertTrue(
     preparePayResultAfter.status === PreparePayResultType.AlreadyConfirmed,
   );
diff --git a/packages/taler-wallet-core/src/bank-api-client.ts 
b/packages/taler-wallet-core/src/bank-api-client.ts
index 8f82d7e7f..557b8c315 100644
--- a/packages/taler-wallet-core/src/bank-api-client.ts
+++ b/packages/taler-wallet-core/src/bank-api-client.ts
@@ -34,11 +34,7 @@ import {
   TalerErrorCode,
 } from "@gnu-taler/taler-util";
 import { TalerError } from "./errors.js";
-import {
-  HttpRequestLibrary,
-  readSuccessResponseJsonOrErrorCode,
-  readSuccessResponseJsonOrThrow,
-} from "./index.browser.js";
+import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from 
"./util/http.js";
 
 const logger = new Logger("bank-api-client.ts");
 
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 16ae2cf8d..b019be67a 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -98,11 +98,11 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
  */
 export const WALLET_DB_MINOR_VERSION = 2;
 
-export namespace OperationStatusRange {
-  export const ACTIVE_START = 10;
-  export const ACTIVE_END = 29;
-  export const DORMANT_START = 50;
-  export const DORMANT_END = 69;
+export enum OperationStatusRange {
+  ACTIVE_START = 10,
+  ACTIVE_END = 29,
+  DORMANT_START = 50,
+  DORMANT_END = 69,
 }
 
 /**
@@ -741,93 +741,6 @@ export interface CoinAllocation {
   amount: AmountString;
 }
 
-export enum ProposalStatus {
-  /**
-   * Not downloaded yet.
-   */
-  Downloading = "downloading",
-  /**
-   * Proposal downloaded, but the user needs to accept/reject it.
-   */
-  Proposed = "proposed",
-  /**
-   * The user has accepted the proposal.
-   */
-  Accepted = "accepted",
-  /**
-   * The user has rejected the proposal.
-   */
-  Refused = "refused",
-  /**
-   * Downloading or processing the proposal has failed permanently.
-   */
-  PermanentlyFailed = "permanently-failed",
-  /**
-   * Downloaded proposal was detected as a re-purchase.
-   */
-  Repurchase = "repurchase",
-}
-
-export interface ProposalDownload {
-  /**
-   * The contract that was offered by the merchant.
-   */
-  contractTermsRaw: any;
-
-  /**
-   * Extracted / parsed data from the contract terms.
-   *
-   * FIXME: Do we need to store *all* that data in duplicate?
-   */
-  contractData: WalletContractData;
-}
-
-/**
- * Record for a downloaded order, stored in the wallet's database.
- */
-export interface ProposalRecord {
-  orderId: string;
-
-  merchantBaseUrl: string;
-
-  /**
-   * Downloaded data from the merchant.
-   */
-  download: ProposalDownload | undefined;
-
-  /**
-   * Unique ID when the order is stored in the wallet DB.
-   */
-  proposalId: string;
-
-  /**
-   * Timestamp (in ms) of when the record
-   * was created.
-   */
-  timestamp: TalerProtocolTimestamp;
-
-  /**
-   * Private key for the nonce.
-   */
-  noncePriv: string;
-
-  /**
-   * Public key for the nonce.
-   */
-  noncePub: string;
-
-  claimToken: string | undefined;
-
-  proposalStatus: ProposalStatus;
-
-  repurchaseProposalId: string | undefined;
-
-  /**
-   * Session ID we got when downloading the contract.
-   */
-  downloadSessionId: string | undefined;
-}
-
 /**
  * Status of a tip we got from a merchant.
  */
@@ -1113,23 +1026,132 @@ export interface WalletContractData {
   deliveryLocation: Location | undefined;
 }
 
-export enum AbortStatus {
-  None = "none",
-  AbortRefund = "abort-refund",
-  AbortFinished = "abort-finished",
+export enum PurchaseStatus {
+  /**
+   * Not downloaded yet.
+   */
+  DownloadingProposal = OperationStatusRange.ACTIVE_START,
+
+  /**
+   * The user has accepted the proposal.
+   */
+  Paying = OperationStatusRange.ACTIVE_START + 1,
+
+  AbortingWithRefund = OperationStatusRange.ACTIVE_START + 2,
+
+  /**
+   * Paying a second time, likely with different session ID
+   */
+  PayingReplay = OperationStatusRange.ACTIVE_START + 3,
+
+  /**
+   * Query for refunds (until query succeeds).
+   */
+  QueryingRefund = OperationStatusRange.ACTIVE_START + 4,
+
+  /**
+   * Query for refund (until auto-refund deadline is reached).
+   */
+  QueryingAutoRefund = OperationStatusRange.ACTIVE_START + 5,
+
+  /**
+   * Proposal downloaded, but the user needs to accept/reject it.
+   */
+  Proposed = OperationStatusRange.DORMANT_START,
+
+  /**
+   * The user has rejected the proposal.
+   */
+  ProposalRefused = OperationStatusRange.DORMANT_START + 1,
+
+  /**
+   * Downloading or processing the proposal has failed permanently.
+   */
+  ProposalDownloadFailed = OperationStatusRange.DORMANT_START + 2,
+
+  /**
+   * Downloaded proposal was detected as a re-purchase.
+   */
+  RepurchaseDetected = OperationStatusRange.DORMANT_START + 3,
+
+  /**
+   * The payment has been aborted.
+   */
+  PaymentAbortFinished = OperationStatusRange.DORMANT_START + 4,
+
+  /**
+   * Payment was successful.
+   */
+  Paid = OperationStatusRange.DORMANT_START + 5,
+}
+
+export interface ProposalDownload {
+  /**
+   * The contract that was offered by the merchant.
+   */
+  contractTermsRaw: any;
+
+  /**
+   * Extracted / parsed data from the contract terms.
+   *
+   * FIXME: Do we need to store *all* that data in duplicate?
+   */
+  contractData: WalletContractData;
+}
+
+export interface PurchasePayInfo {
+  payCoinSelection: PayCoinSelection;
+  totalPayCost: AmountJson;
+  payCoinSelectionUid: string;
+
+  /**
+   * Deposit permissions, available once the user has accepted the payment.
+   *
+   * This value is cached and derived from payCoinSelection.
+   *
+   * FIXME: Should probably be cached somewhere else, maybe not even in DB!
+   */
+  coinDepositPermissions: CoinDepositPermission[] | undefined;
 }
 
 /**
  * Record that stores status information about one purchase, starting from when
  * the customer accepts a proposal.  Includes refund status if applicable.
+ *
+ * FIXME: Should have a single "status" field.
  */
 export interface PurchaseRecord {
   /**
    * Proposal ID for this purchase.  Uniquely identifies the
    * purchase and the proposal.
+   * Assigned by the wallet.
    */
   proposalId: string;
 
+  /**
+   * Order ID, assigned by the merchant.
+   */
+  orderId: string;
+
+  merchantBaseUrl: string;
+
+  /**
+   * Claim token used when downloading the contract terms.
+   */
+  claimToken: string | undefined;
+
+  /**
+   * Session ID we got when downloading the contract.
+   */
+  downloadSessionId: string | undefined;
+
+  /**
+   * If this purchase is a repurchase, this field identifies the original 
purchase.
+   */
+  repurchaseProposalId: string | undefined;
+
+  purchaseStatus: PurchaseStatus;
+
   /**
    * Private key for the nonce.
    */
@@ -1146,18 +1168,9 @@ export interface PurchaseRecord {
    * FIXME:  Move this into another object store,
    * to improve read/write perf on purchases.
    */
-  download: ProposalDownload;
-
-  /**
-   * Deposit permissions, available once the user has accepted the payment.
-   *
-   * This value is cached and derived from payCoinSelection.
-   */
-  coinDepositPermissions: CoinDepositPermission[] | undefined;
-
-  payCoinSelection: PayCoinSelection;
+  download: ProposalDownload | undefined;
 
-  payCoinSelectionUid: string;
+  payInfo: PurchasePayInfo | undefined;
 
   /**
    * Pending removals from pay coin selection.
@@ -1169,8 +1182,6 @@ export interface PurchaseRecord {
    */
   pendingRemovedCoinPubs?: string[];
 
-  totalPayCost: AmountJson;
-
   /**
    * Timestamp of the first time that sending a payment to the merchant
    * for this purchase was successful.
@@ -1181,11 +1192,16 @@ export interface PurchaseRecord {
 
   merchantPaySig: string | undefined;
 
+  /**
+   * When was the purchase record created?
+   */
+  timestamp: TalerProtocolTimestamp;
+
   /**
    * When was the purchase made?
    * Refers to the time that the user accepted.
    */
-  timestampAccept: TalerProtocolTimestamp;
+  timestampAccept: TalerProtocolTimestamp | undefined;
 
   /**
    * Pending refunds for the purchase.  A refund is pending
@@ -1206,18 +1222,6 @@ export interface PurchaseRecord {
    */
   lastSessionId: string | undefined;
 
-  /**
-   * Do we still need to post the deposit permissions to the merchant?
-   * Set for the first payment, or on re-plays.
-   */
-  paymentSubmitPending: boolean;
-
-  /**
-   * Do we need to query the merchant for the refund status
-   * of the payment?
-   */
-  refundQueryRequested: boolean;
-
   /**
    * Continue querying the refund status until this deadline has expired.
    */
@@ -1227,18 +1231,7 @@ export interface PurchaseRecord {
    * How much merchant has refund to be taken but the wallet
    * did not picked up yet
    */
-  refundAwaiting: AmountJson | undefined;
-
-  /**
-   * Is the payment frozen?  I.e. did we encounter
-   * an error where it doesn't make sense to retry.
-   */
-  payFrozen?: boolean;
-
-  /**
-   * FIXME: How does this interact with payFrozen?
-   */
-  abortStatus: AbortStatus;
+  refundAmountAwaiting: AmountJson | undefined;
 }
 
 export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
@@ -1389,8 +1382,6 @@ export interface WithdrawalGroupRecord {
 
   /**
    * Current status of the reserve.
-   *
-   * FIXME: Wrong name!
    */
   status: WithdrawalGroupStatus;
 
@@ -1923,16 +1914,6 @@ export const WalletStoresV1 = {
     }),
     {},
   ),
-  proposals: describeStore(
-    "proposals",
-    describeContents<ProposalRecord>({ keyPath: "proposalId" }),
-    {
-      byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
-        "merchantBaseUrl",
-        "orderId",
-      ]),
-    },
-  ),
   refreshGroups: describeStore(
     "refreshGroups",
     describeContents<RefreshGroupRecord>({
@@ -1953,14 +1934,20 @@ export const WalletStoresV1 = {
     "purchases",
     describeContents<PurchaseRecord>({ keyPath: "proposalId" }),
     {
+      byStatus: describeIndex("byStatus", "purchaseStatus"),
       byFulfillmentUrl: describeIndex(
         "byFulfillmentUrl",
         "download.contractData.fulfillmentUrl",
       ),
+      // FIXME: Deduplicate!
       byMerchantUrlAndOrderId: describeIndex("byMerchantUrlAndOrderId", [
         "download.contractData.merchantBaseUrl",
         "download.contractData.orderId",
       ]),
+      byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
+        "merchantBaseUrl",
+        "orderId",
+      ]),
     },
   ),
   tips: describeStore(
diff --git a/packages/taler-wallet-core/src/dbless.ts 
b/packages/taler-wallet-core/src/dbless.ts
index ff7870435..9d709e8e3 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -26,6 +26,9 @@
  * Imports.
  */
 import {
+  AbsoluteTime,
+  AgeRestriction,
+  AmountJson,
   Amounts,
   AmountString,
   codecForAny,
@@ -35,7 +38,6 @@ import {
   codecForExchangeRevealResponse,
   codecForWithdrawResponse,
   DenominationPubKey,
-  eddsaGetPublic,
   encodeCrock,
   ExchangeMeltRequest,
   ExchangeProtocolVersion,
@@ -44,29 +46,15 @@ import {
   hashWire,
   Logger,
   parsePaytoUri,
-  AbsoluteTime,
   UnblindedSignature,
-  BankWithdrawDetails,
-  parseWithdrawUri,
-  AmountJson,
-  AgeRestriction,
 } from "@gnu-taler/taler-util";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import { DenominationRecord } from "./db.js";
-import {
-  assembleRefreshRevealRequest,
-  ExchangeInfo,
-  getBankWithdrawalInfo,
-  HttpRequestLibrary,
-  isWithdrawableDenom,
-  readSuccessResponseJsonOrThrow,
-} from "./index.browser.js";
-import {
-  BankAccessApi,
-  BankApi,
-  BankServiceHandle,
-  getBankStatusUrl,
-} from "./index.js";
+import { BankAccessApi, BankApi, BankServiceHandle } from 
"./bank-api-client.js";
+import { HttpRequestLibrary, readSuccessResponseJsonOrThrow } from 
"./util/http.js";
+import { getBankStatusUrl, getBankWithdrawalInfo, isWithdrawableDenom } from 
"./operations/withdraw.js";
+import { ExchangeInfo } from "./operations/exchanges.js";
+import { assembleRefreshRevealRequest } from "./operations/refresh.js";
 
 const logger = new Logger("dbless.ts");
 
diff --git a/packages/taler-wallet-core/src/index.ts 
b/packages/taler-wallet-core/src/index.ts
index 4e419503b..0e01e3517 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -47,7 +47,6 @@ export * from "./wallet-api-types.js";
 export * from "./wallet.js";
 
 export * from "./operations/backup/index.js";
-export { makeEventId } from "./operations/transactions.js";
 
 export * from "./operations/exchanges.js";
 
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts 
b/packages/taler-wallet-core/src/internal-wallet-state.ts
index b8415a469..6c7d943cb 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -37,6 +37,9 @@ import {
   TalerProtocolTimestamp,
   CancellationToken,
   DenominationInfo,
+  RefreshGroupId,
+  CoinPublicKey,
+  RefreshReason,
 } from "@gnu-taler/taler-util";
 import { CryptoDispatcher } from "./crypto/workers/cryptoDispatcher.js";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
@@ -74,6 +77,20 @@ export interface MerchantOperations {
   ): Promise<MerchantInfo>;
 }
 
+export interface RefreshOperations {
+  createRefreshGroup(
+    ws: InternalWalletState,
+    tx: GetReadWriteAccess<{
+      denominations: typeof WalletStoresV1.denominations;
+      coins: typeof WalletStoresV1.coins;
+      refreshGroups: typeof WalletStoresV1.refreshGroups;
+      coinAvailability: typeof WalletStoresV1.coinAvailability;
+    }>,
+    oldCoinPubs: CoinPublicKey[],
+    reason: RefreshReason,
+  ): Promise<RefreshGroupId>;
+}
+
 /**
  * Interface for exchange-related operations.
  */
@@ -172,6 +189,7 @@ export interface InternalWalletState {
   exchangeOps: ExchangeOperations;
   recoupOps: RecoupOperations;
   merchantOps: MerchantOperations;
+  refreshOps: RefreshOperations;
 
   getDenomInfo(
     ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts 
b/packages/taler-wallet-core/src/operations/backup/export.ts
index c8454a62f..d16b344f6 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -37,7 +37,7 @@ import {
   BackupExchangeDetails,
   BackupExchangeWireFee,
   BackupOperationStatus,
-  BackupProposal,
+  BackupPayInfo,
   BackupProposalStatus,
   BackupPurchase,
   BackupRecoupGroup,
@@ -62,12 +62,10 @@ import {
   WalletBackupContentV1,
 } from "@gnu-taler/taler-util";
 import {
-  AbortStatus,
   CoinSourceType,
   CoinStatus,
   DenominationRecord,
-  OperationStatus,
-  ProposalStatus,
+  PurchaseStatus,
   RefreshCoinStatus,
   RefundState,
   WALLET_BACKUP_STATE_KEY,
@@ -92,7 +90,6 @@ export async function exportBackup(
       x.coins,
       x.denominations,
       x.purchases,
-      x.proposals,
       x.refreshGroups,
       x.backupProviders,
       x.tips,
@@ -109,7 +106,6 @@ export async function exportBackup(
         [url: string]: BackupDenomination[];
       } = {};
       const backupPurchases: BackupPurchase[] = [];
-      const backupProposals: BackupProposal[] = [];
       const backupRefreshGroups: BackupRefreshGroup[] = [];
       const backupBackupProviders: BackupBackupProvider[] = [];
       const backupTips: BackupTip[] = [];
@@ -385,65 +381,61 @@ export async function exportBackup(
           }
         }
 
-        backupPurchases.push({
-          contract_terms_raw: purch.download.contractTermsRaw,
-          auto_refund_deadline: purch.autoRefundDeadline,
-          merchant_pay_sig: purch.merchantPaySig,
-          pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
-            coin_pub: x,
-            contribution: Amounts.stringify(
-              purch.payCoinSelection.coinContributions[i],
-            ),
-          })),
-          proposal_id: purch.proposalId,
-          refunds,
-          timestamp_accept: purch.timestampAccept,
-          timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
-          abort_status:
-            purch.abortStatus === AbortStatus.None
-              ? undefined
-              : purch.abortStatus,
-          nonce_priv: purch.noncePriv,
-          merchant_sig: purch.download.contractData.merchantSig,
-          total_pay_cost: Amounts.stringify(purch.totalPayCost),
-          pay_coins_uid: purch.payCoinSelectionUid,
-        });
-      });
-
-      await tx.proposals.iter().forEach((prop) => {
-        if (purchaseProposalIdSet.has(prop.proposalId)) {
-          return;
-        }
         let propStatus: BackupProposalStatus;
-        switch (prop.proposalStatus) {
-          case ProposalStatus.Accepted:
+        switch (purch.purchaseStatus) {
+          case PurchaseStatus.Paid:
+            propStatus = BackupProposalStatus.Paid;
             return;
-          case ProposalStatus.Downloading:
-          case ProposalStatus.Proposed:
+          case PurchaseStatus.DownloadingProposal:
+          case PurchaseStatus.Proposed:
             propStatus = BackupProposalStatus.Proposed;
             break;
-          case ProposalStatus.PermanentlyFailed:
+          case PurchaseStatus.ProposalDownloadFailed:
             propStatus = BackupProposalStatus.PermanentlyFailed;
             break;
-          case ProposalStatus.Refused:
+          case PurchaseStatus.ProposalRefused:
             propStatus = BackupProposalStatus.Refused;
             break;
-          case ProposalStatus.Repurchase:
+          case PurchaseStatus.RepurchaseDetected:
             propStatus = BackupProposalStatus.Repurchase;
             break;
+          default:
+            throw Error();
         }
-        backupProposals.push({
-          claim_token: prop.claimToken,
-          nonce_priv: prop.noncePriv,
-          proposal_id: prop.noncePriv,
+
+        const payInfo = purch.payInfo;
+        let backupPayInfo: BackupPayInfo | undefined = undefined;
+        if (payInfo) {
+          backupPayInfo = {
+            pay_coins: payInfo.payCoinSelection.coinPubs.map((x, i) => ({
+              coin_pub: x,
+              contribution: Amounts.stringify(
+                payInfo.payCoinSelection.coinContributions[i],
+              ),
+            })),
+            total_pay_cost: Amounts.stringify(payInfo.totalPayCost),
+            pay_coins_uid: payInfo.payCoinSelectionUid,
+          };
+        }
+
+        backupPurchases.push({
+          contract_terms_raw: purch.download?.contractTermsRaw,
+          auto_refund_deadline: purch.autoRefundDeadline,
+          merchant_pay_sig: purch.merchantPaySig,
+          pay_info: backupPayInfo,
+          proposal_id: purch.proposalId,
+          refunds,
+          timestamp_accepted: purch.timestampAccept,
+          timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
+          nonce_priv: purch.noncePriv,
+          merchant_sig: purch.download?.contractData.merchantSig,
+          claim_token: purch.claimToken,
+          merchant_base_url: purch.merchantBaseUrl,
+          order_id: purch.orderId,
           proposal_status: propStatus,
-          repurchase_proposal_id: prop.repurchaseProposalId,
-          timestamp: prop.timestamp,
-          contract_terms_raw: prop.download?.contractTermsRaw,
-          download_session_id: prop.downloadSessionId,
-          merchant_base_url: prop.merchantBaseUrl,
-          order_id: prop.orderId,
-          merchant_sig: prop.download?.contractData.merchantSig,
+          repurchase_proposal_id: purch.repurchaseProposalId,
+          download_session_id: purch.downloadSessionId,
+          timestamp_proposed: purch.timestamp,
         });
       });
 
@@ -498,7 +490,6 @@ export async function exportBackup(
         wallet_root_pub: bs.walletRootPub,
         backup_providers: backupBackupProviders,
         current_device_id: bs.deviceId,
-        proposals: backupProposals,
         purchases: backupPurchases,
         recoup_groups: backupRecoupGroups,
         refresh_groups: backupRefreshGroups,
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index fb747ef1c..bb5fe56e2 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -21,8 +21,8 @@ import {
   BackupCoin,
   BackupCoinSourceType,
   BackupDenomSel,
+  BackupPayInfo,
   BackupProposalStatus,
-  BackupPurchase,
   BackupRefreshReason,
   BackupRefundState,
   BackupWgType,
@@ -37,7 +37,6 @@ import {
   WireInfo,
 } from "@gnu-taler/taler-util";
 import {
-  AbortStatus,
   CoinRecord,
   CoinSource,
   CoinSourceType,
@@ -47,29 +46,24 @@ import {
   DenomSelectionState,
   OperationStatus,
   ProposalDownload,
-  ProposalStatus,
+  PurchaseStatus,
+  PurchasePayInfo,
   RefreshCoinStatus,
   RefreshSessionRecord,
   RefundState,
-  ReserveBankInfo,
-  WithdrawalGroupStatus,
   WalletContractData,
   WalletRefundItem,
   WalletStoresV1,
   WgInfo,
+  WithdrawalGroupStatus,
   WithdrawalRecordType,
 } from "../../db.js";
 import { InternalWalletState } from "../../internal-wallet-state.js";
 import { assertUnreachable } from "../../util/assertUnreachable.js";
-import {
-  checkDbInvariant,
-  checkLogicInvariant,
-} from "../../util/invariants.js";
+import { checkLogicInvariant } from "../../util/invariants.js";
 import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import { RetryInfo } from "../../util/retries.js";
-import { makeCoinAvailable } from "../../wallet.js";
+import { makeCoinAvailable, makeEventId, TombstoneTag } from "../common.js";
 import { getExchangeDetails } from "../exchanges.js";
-import { makeEventId, TombstoneTag } from "../transactions.js";
 import { provideBackupState } from "./state.js";
 
 const logger = new Logger("operations/backup/import.ts");
@@ -95,10 +89,10 @@ async function recoverPayCoinSelection(
     denominations: typeof WalletStoresV1.denominations;
   }>,
   contractData: WalletContractData,
-  backupPurchase: BackupPurchase,
+  payInfo: BackupPayInfo,
 ): Promise<PayCoinSelection> {
-  const coinPubs: string[] = backupPurchase.pay_coins.map((x) => x.coin_pub);
-  const coinContributions: AmountJson[] = backupPurchase.pay_coins.map((x) =>
+  const coinPubs: string[] = payInfo.pay_coins.map((x) => x.coin_pub);
+  const coinContributions: AmountJson[] = payInfo.pay_coins.map((x) =>
     Amounts.parseOrThrow(x.contribution),
   );
 
@@ -316,7 +310,6 @@ export async function importBackup(
       x.coinAvailability,
       x.denominations,
       x.purchases,
-      x.proposals,
       x.refreshGroups,
       x.backupProviders,
       x.tips,
@@ -560,113 +553,6 @@ export async function importBackup(
         }
       }
 
-      for (const backupProposal of backupBlob.proposals) {
-        const ts = makeEventId(
-          TombstoneTag.DeletePayment,
-          backupProposal.proposal_id,
-        );
-        if (tombstoneSet.has(ts)) {
-          continue;
-        }
-        const existingProposal = await tx.proposals.get(
-          backupProposal.proposal_id,
-        );
-        if (!existingProposal) {
-          let download: ProposalDownload | undefined;
-          let proposalStatus: ProposalStatus;
-          switch (backupProposal.proposal_status) {
-            case BackupProposalStatus.Proposed:
-              if (backupProposal.contract_terms_raw) {
-                proposalStatus = ProposalStatus.Proposed;
-              } else {
-                proposalStatus = ProposalStatus.Downloading;
-              }
-              break;
-            case BackupProposalStatus.Refused:
-              proposalStatus = ProposalStatus.Refused;
-              break;
-            case BackupProposalStatus.Repurchase:
-              proposalStatus = ProposalStatus.Repurchase;
-              break;
-            case BackupProposalStatus.PermanentlyFailed:
-              proposalStatus = ProposalStatus.PermanentlyFailed;
-              break;
-          }
-          if (backupProposal.contract_terms_raw) {
-            checkDbInvariant(!!backupProposal.merchant_sig);
-            const parsedContractTerms = codecForContractTerms().decode(
-              backupProposal.contract_terms_raw,
-            );
-            const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
-            const contractTermsHash =
-              cryptoComp.proposalIdToContractTermsHash[
-                backupProposal.proposal_id
-              ];
-            let maxWireFee: AmountJson;
-            if (parsedContractTerms.max_wire_fee) {
-              maxWireFee = Amounts.parseOrThrow(
-                parsedContractTerms.max_wire_fee,
-              );
-            } else {
-              maxWireFee = Amounts.getZero(amount.currency);
-            }
-            download = {
-              contractData: {
-                amount,
-                contractTermsHash: contractTermsHash,
-                fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
-                merchantBaseUrl: parsedContractTerms.merchant_base_url,
-                merchantPub: parsedContractTerms.merchant_pub,
-                merchantSig: backupProposal.merchant_sig,
-                orderId: parsedContractTerms.order_id,
-                summary: parsedContractTerms.summary,
-                autoRefund: parsedContractTerms.auto_refund,
-                maxWireFee,
-                payDeadline: parsedContractTerms.pay_deadline,
-                refundDeadline: parsedContractTerms.refund_deadline,
-                wireFeeAmortization:
-                  parsedContractTerms.wire_fee_amortization || 1,
-                allowedAuditors: parsedContractTerms.auditors.map((x) => ({
-                  auditorBaseUrl: x.url,
-                  auditorPub: x.auditor_pub,
-                })),
-                allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
-                  exchangeBaseUrl: x.url,
-                  exchangePub: x.master_pub,
-                })),
-                timestamp: parsedContractTerms.timestamp,
-                wireMethod: parsedContractTerms.wire_method,
-                wireInfoHash: parsedContractTerms.h_wire,
-                maxDepositFee: Amounts.parseOrThrow(
-                  parsedContractTerms.max_fee,
-                ),
-                merchant: parsedContractTerms.merchant,
-                products: parsedContractTerms.products,
-                summaryI18n: parsedContractTerms.summary_i18n,
-                deliveryDate: parsedContractTerms.delivery_date,
-                deliveryLocation: parsedContractTerms.delivery_location,
-              },
-              contractTermsRaw: backupProposal.contract_terms_raw,
-            };
-          }
-          await tx.proposals.put({
-            claimToken: backupProposal.claim_token,
-            merchantBaseUrl: backupProposal.merchant_base_url,
-            timestamp: backupProposal.timestamp,
-            orderId: backupProposal.order_id,
-            noncePriv: backupProposal.nonce_priv,
-            noncePub:
-              cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
-            proposalId: backupProposal.proposal_id,
-            repurchaseProposalId: backupProposal.repurchase_proposal_id,
-            download,
-            proposalStatus,
-            // FIXME!
-            downloadSessionId: undefined,
-          });
-        }
-      }
-
       for (const backupPurchase of backupBlob.purchases) {
         const ts = makeEventId(
           TombstoneTag.DeletePayment,
@@ -678,6 +564,14 @@ export async function importBackup(
         const existingPurchase = await tx.purchases.get(
           backupPurchase.proposal_id,
         );
+        let proposalStatus: PurchaseStatus;
+        switch (backupPurchase.proposal_status) {
+          case BackupProposalStatus.Paid:
+            proposalStatus = PurchaseStatus.Paid;
+            break;
+          default:
+            throw Error();
+        }
         if (!existingPurchase) {
           const refunds: { [refundKey: string]: WalletRefundItem } = {};
           for (const backupRefund of backupPurchase.refunds) {
@@ -721,25 +615,6 @@ export async function importBackup(
                 break;
             }
           }
-          let abortStatus: AbortStatus;
-          switch (backupPurchase.abort_status) {
-            case "abort-finished":
-              abortStatus = AbortStatus.AbortFinished;
-              break;
-            case "abort-refund":
-              abortStatus = AbortStatus.AbortRefund;
-              break;
-            case undefined:
-              abortStatus = AbortStatus.None;
-              break;
-            default:
-              logger.warn(
-                `got backup purchase abort_status ${j2s(
-                  backupPurchase.abort_status,
-                )}`,
-              );
-              throw Error("not reachable");
-          }
           const parsedContractTerms = codecForContractTerms().decode(
             backupPurchase.contract_terms_raw,
           );
@@ -761,7 +636,7 @@ export async function importBackup(
               fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
               merchantBaseUrl: parsedContractTerms.merchant_base_url,
               merchantPub: parsedContractTerms.merchant_pub,
-              merchantSig: backupPurchase.merchant_sig,
+              merchantSig: backupPurchase.merchant_sig!,
               orderId: parsedContractTerms.order_id,
               summary: parsedContractTerms.summary,
               autoRefund: parsedContractTerms.auto_refund,
@@ -790,33 +665,46 @@ export async function importBackup(
             },
             contractTermsRaw: backupPurchase.contract_terms_raw,
           };
+
+          let payInfo: PurchasePayInfo | undefined = undefined;
+          if (backupPurchase.pay_info) {
+            payInfo = {
+              coinDepositPermissions: undefined,
+              payCoinSelection: await recoverPayCoinSelection(
+                tx,
+                download.contractData,
+                backupPurchase.pay_info,
+              ),
+              payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,
+              totalPayCost: Amounts.parseOrThrow(
+                backupPurchase.pay_info.total_pay_cost,
+              ),
+            };
+          }
+
           await tx.purchases.put({
             proposalId: backupPurchase.proposal_id,
             noncePriv: backupPurchase.nonce_priv,
             noncePub:
               cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
             autoRefundDeadline: TalerProtocolTimestamp.never(),
-            refundAwaiting: undefined,
-            timestampAccept: backupPurchase.timestamp_accept,
+            timestampAccept: backupPurchase.timestamp_accepted,
             timestampFirstSuccessfulPay:
               backupPurchase.timestamp_first_successful_pay,
             timestampLastRefundStatus: undefined,
             merchantPaySig: backupPurchase.merchant_pay_sig,
             lastSessionId: undefined,
-            abortStatus,
             download,
-            paymentSubmitPending:
-              !backupPurchase.timestamp_first_successful_pay,
-            refundQueryRequested: false,
-            payCoinSelection: await recoverPayCoinSelection(
-              tx,
-              download.contractData,
-              backupPurchase,
-            ),
-            coinDepositPermissions: undefined,
-            totalPayCost: Amounts.parseOrThrow(backupPurchase.total_pay_cost),
             refunds,
-            payCoinSelectionUid: backupPurchase.pay_coins_uid,
+            claimToken: backupPurchase.claim_token,
+            downloadSessionId: backupPurchase.download_session_id,
+            merchantBaseUrl: backupPurchase.merchant_base_url,
+            orderId: backupPurchase.order_id,
+            payInfo,
+            refundAmountAwaiting: undefined,
+            repurchaseProposalId: backupPurchase.repurchase_proposal_id,
+            purchaseStatus: proposalStatus,
+            timestamp: backupPurchase.timestamp_proposed,
           });
         }
       }
@@ -948,7 +836,6 @@ export async function importBackup(
           await tx.depositGroups.delete(rest[0]);
         } else if (type === TombstoneTag.DeletePayment) {
           await tx.purchases.delete(rest[0]);
-          await tx.proposals.delete(rest[0]);
         } else if (type === TombstoneTag.DeleteRefreshGroup) {
           await tx.refreshGroups.delete(rest[0]);
         } else if (type === TombstoneTag.DeleteRefund) {
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index fc84ce4ef..3d3ebf04a 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -96,7 +96,7 @@ import {
   checkPaymentByProposalId,
   confirmPay,
   preparePayForUri,
-} from "../pay.js";
+} from "../pay-merchant.js";
 import { exportBackup } from "./export.js";
 import { BackupCryptoPrecomputedData, importBackup } from "./import.js";
 import { getWalletBackupState, provideBackupState } from "./state.js";
@@ -193,15 +193,6 @@ async function computeBackupCryptoData(
       eddsaGetPublic(decodeCrock(backupWg.reserve_priv)),
     );
   }
-  for (const prop of backupContent.proposals) {
-    const { h: contractTermsHash } = await cryptoApi.hashString({
-      str: canonicalJson(prop.contract_terms_raw),
-    });
-    const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv)));
-    cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub;
-    cryptoData.proposalIdToContractTermsHash[prop.proposal_id] =
-      contractTermsHash;
-  }
   for (const purch of backupContent.purchases) {
     const { h: contractTermsHash } = await cryptoApi.hashString({
       str: canonicalJson(purch.contract_terms_raw),
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index 6d54503a1..9f235c9b4 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -17,38 +17,272 @@
 /**
  * Imports.
  */
-import { TalerErrorDetail, TalerErrorCode } from "@gnu-taler/taler-util";
-import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
-import { TalerError, getErrorDetailFromException } from "../errors.js";
+import {
+  AmountJson,
+  Amounts,
+  j2s,
+  Logger,
+  RefreshReason,
+  TalerErrorCode,
+  TalerErrorDetail,
+  TransactionType,
+} from "@gnu-taler/taler-util";
+import { WalletStoresV1, CoinStatus, CoinRecord } from "../db.js";
+import { makeErrorDetail, TalerError } from "../errors.js";
+import { InternalWalletState } from "../internal-wallet-state.js";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
+import { GetReadWriteAccess } from "../util/query.js";
+import {
+  OperationAttemptResult,
+  OperationAttemptResultType,
+  RetryInfo,
+} from "../util/retries.js";
+import { createRefreshGroup } from "./refresh.js";
 
-/**
- * Run an operation and call the onOpError callback
- * when there was an exception or operation error that must be reported.
- * The cause will be re-thrown to the caller.
- */
-export async function guardOperationException<T>(
-  op: () => Promise<T>,
-  onOpError: (e: TalerErrorDetail) => Promise<void>,
-): Promise<T> {
+const logger = new Logger("operations/common.ts");
+
+export interface CoinsSpendInfo {
+  coinPubs: string[];
+  contributions: AmountJson[];
+  refreshReason: RefreshReason;
+  /**
+   * Identifier for what the coin has been spent for.
+   */
+  allocationId: string;
+}
+
+export async function makeCoinAvailable(
+  ws: InternalWalletState,
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  coinRecord: CoinRecord,
+): Promise<void> {
+  checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
+  const existingCoin = await tx.coins.get(coinRecord.coinPub);
+  if (existingCoin) {
+    return;
+  }
+  const denom = await tx.denominations.get([
+    coinRecord.exchangeBaseUrl,
+    coinRecord.denomPubHash,
+  ]);
+  checkDbInvariant(!!denom);
+  const ageRestriction = coinRecord.maxAge;
+  let car = await tx.coinAvailability.get([
+    coinRecord.exchangeBaseUrl,
+    coinRecord.denomPubHash,
+    ageRestriction,
+  ]);
+  if (!car) {
+    car = {
+      maxAge: ageRestriction,
+      amountFrac: denom.amountFrac,
+      amountVal: denom.amountVal,
+      currency: denom.currency,
+      denomPubHash: denom.denomPubHash,
+      exchangeBaseUrl: denom.exchangeBaseUrl,
+      freshCoinCount: 0,
+    };
+  }
+  car.freshCoinCount++;
+  await tx.coins.put(coinRecord);
+  await tx.coinAvailability.put(car);
+}
+
+export async function spendCoins(
+  ws: InternalWalletState,
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
+    refreshGroups: typeof WalletStoresV1.refreshGroups;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  csi: CoinsSpendInfo,
+): Promise<void> {
+  for (let i = 0; i < csi.coinPubs.length; i++) {
+    const coin = await tx.coins.get(csi.coinPubs[i]);
+    if (!coin) {
+      throw Error("coin allocated for payment doesn't exist anymore");
+    }
+    const coinAvailability = await tx.coinAvailability.get([
+      coin.exchangeBaseUrl,
+      coin.denomPubHash,
+      coin.maxAge,
+    ]);
+    checkDbInvariant(!!coinAvailability);
+    const contrib = csi.contributions[i];
+    if (coin.status !== CoinStatus.Fresh) {
+      const alloc = coin.allocation;
+      if (!alloc) {
+        continue;
+      }
+      if (alloc.id !== csi.allocationId) {
+        // FIXME: assign error code
+        throw Error("conflicting coin allocation (id)");
+      }
+      if (0 !== Amounts.cmp(alloc.amount, contrib)) {
+        // FIXME: assign error code
+        throw Error("conflicting coin allocation (contrib)");
+      }
+      continue;
+    }
+    coin.status = CoinStatus.Dormant;
+    coin.allocation = {
+      id: csi.allocationId,
+      amount: Amounts.stringify(contrib),
+    };
+    const remaining = Amounts.sub(coin.currentAmount, contrib);
+    if (remaining.saturated) {
+      throw Error("not enough remaining balance on coin for payment");
+    }
+    coin.currentAmount = remaining.amount;
+    checkDbInvariant(!!coinAvailability);
+    if (coinAvailability.freshCoinCount === 0) {
+      throw Error(
+        `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
+      );
+    }
+    coinAvailability.freshCoinCount--;
+    await tx.coins.put(coin);
+    await tx.coinAvailability.put(coinAvailability);
+  }
+  const refreshCoinPubs = csi.coinPubs.map((x) => ({
+    coinPub: x,
+  }));
+  await ws.refreshOps.createRefreshGroup(
+    ws,
+    tx,
+    refreshCoinPubs,
+    RefreshReason.PayMerchant,
+  );
+}
+
+export async function storeOperationError(
+  ws: InternalWalletState,
+  pendingTaskId: string,
+  e: TalerErrorDetail,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.operationRetries])
+    .runReadWrite(async (tx) => {
+      let retryRecord = await tx.operationRetries.get(pendingTaskId);
+      if (!retryRecord) {
+        retryRecord = {
+          id: pendingTaskId,
+          lastError: e,
+          retryInfo: RetryInfo.reset(),
+        };
+      } else {
+        retryRecord.lastError = e;
+        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+      }
+      await tx.operationRetries.put(retryRecord);
+    });
+}
+
+export async function storeOperationPending(
+  ws: InternalWalletState,
+  pendingTaskId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.operationRetries])
+    .runReadWrite(async (tx) => {
+      let retryRecord = await tx.operationRetries.get(pendingTaskId);
+      if (!retryRecord) {
+        retryRecord = {
+          id: pendingTaskId,
+          retryInfo: RetryInfo.reset(),
+        };
+      } else {
+        delete retryRecord.lastError;
+        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
+      }
+      await tx.operationRetries.put(retryRecord);
+    });
+}
+
+export async function runOperationWithErrorReporting(
+  ws: InternalWalletState,
+  opId: string,
+  f: () => Promise<OperationAttemptResult>,
+): Promise<void> {
+  let maybeError: TalerErrorDetail | undefined;
   try {
-    return await op();
-  } catch (e: any) {
-    if (e instanceof CryptoApiStoppedError) {
-      throw e;
+    const resp = await f();
+    switch (resp.type) {
+      case OperationAttemptResultType.Error:
+        return await storeOperationError(ws, opId, resp.errorDetail);
+      case OperationAttemptResultType.Finished:
+        return await storeOperationFinished(ws, opId);
+      case OperationAttemptResultType.Pending:
+        return await storeOperationPending(ws, opId);
+      case OperationAttemptResultType.Longpoll:
+        break;
     }
-    if (
-      e instanceof TalerError &&
-      e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED)
-    ) {
-      throw e;
+  } catch (e) {
+    if (e instanceof TalerError) {
+      logger.warn("operation processed resulted in error");
+      logger.warn(`error was: ${j2s(e.errorDetail)}`);
+      maybeError = e.errorDetail;
+      return await storeOperationError(ws, opId, maybeError!);
+    } else if (e instanceof Error) {
+      // This is a bug, as we expect pending operations to always
+      // do their own error handling and only throw 
WALLET_PENDING_OPERATION_FAILED
+      // or return something.
+      logger.error(`Uncaught exception: ${e.message}`);
+      logger.error(`Stack: ${e.stack}`);
+      maybeError = makeErrorDetail(
+        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+        {
+          stack: e.stack,
+        },
+        `unexpected exception (message: ${e.message})`,
+      );
+      return await storeOperationError(ws, opId, maybeError);
+    } else {
+      logger.error("Uncaught exception, value is not even an error.");
+      maybeError = makeErrorDetail(
+        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+        {},
+        `unexpected exception (not even an error)`,
+      );
+      return await storeOperationError(ws, opId, maybeError);
     }
-    const opErr = getErrorDetailFromException(e);
-    await onOpError(opErr);
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_PENDING_OPERATION_FAILED,
-      {
-        innerError: opErr,
-      },
-    );
   }
 }
+
+export async function storeOperationFinished(
+  ws: InternalWalletState,
+  pendingTaskId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.operationRetries])
+    .runReadWrite(async (tx) => {
+      await tx.operationRetries.delete(pendingTaskId);
+    });
+}
+
+export enum TombstoneTag {
+  DeleteWithdrawalGroup = "delete-withdrawal-group",
+  DeleteReserve = "delete-reserve",
+  DeletePayment = "delete-payment",
+  DeleteTip = "delete-tip",
+  DeleteRefreshGroup = "delete-refresh-group",
+  DeleteDepositGroup = "delete-deposit-group",
+  DeleteRefund = "delete-refund",
+  DeletePeerPullDebit = "delete-peer-pull-debit",
+  DeletePeerPushDebit = "delete-peer-push-debit",
+}
+
+/**
+ * Create an event ID from the type and the primary key for the event.
+ */
+export function makeEventId(
+  type: TransactionType | TombstoneTag,
+  ...args: string[]
+): string {
+  return type + ":" + args.map((x) => encodeURIComponent(x)).join(":");
+}
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 625bc0828..1f7d05d29 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -53,16 +53,15 @@ import {
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 import { OperationAttemptResult } from "../util/retries.js";
-import { spendCoins } from "../wallet.js";
+import { makeEventId, spendCoins } from "./common.js";
 import { getExchangeDetails } from "./exchanges.js";
 import {
   extractContractData,
   generateDepositPermissions,
   getTotalPaymentCost,
   selectPayCoinsNew,
-} from "./pay.js";
+} from "./pay-merchant.js";
 import { getTotalRefreshCost } from "./refresh.js";
-import { makeEventId } from "./transactions.js";
 
 /**
  * Logger.
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index 1dd8660b5..9a6c72577 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -40,7 +40,6 @@ import {
   parsePaytoUri,
   Recoup,
   TalerErrorCode,
-  TalerErrorDetail,
   TalerProtocolDuration,
   TalerProtocolTimestamp,
   URL,
@@ -71,11 +70,9 @@ import {
 import {
   OperationAttemptResult,
   OperationAttemptResultType,
-  RetryInfo,
   runOperationHandlerForResult,
 } from "../util/retries.js";
 import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "../versions.js";
-import { guardOperationException } from "./common.js";
 
 const logger = new Logger("exchanges.ts");
 
diff --git a/packages/taler-wallet-core/src/operations/merchants.ts 
b/packages/taler-wallet-core/src/operations/merchants.ts
index 614478715..f5b3ca38c 100644
--- a/packages/taler-wallet-core/src/operations/merchants.ts
+++ b/packages/taler-wallet-core/src/operations/merchants.ts
@@ -25,7 +25,7 @@ import {
   LibtoolVersion,
 } from "@gnu-taler/taler-util";
 import { InternalWalletState, MerchantInfo } from 
"../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "../index.js";
+import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 
 const logger = new Logger("taler-wallet-core:merchants.ts");
 
@@ -40,7 +40,7 @@ export async function getMerchantInfo(
     return existingInfo;
   }
 
-  const configUrl = new URL("config", canonBaseUrl);
+const configUrl = new URL("config", canonBaseUrl);
   const resp = await ws.http.get(configUrl.href);
 
   const configResp = await readSuccessResponseJsonOrThrow(
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay-merchant.ts
similarity index 60%
rename from packages/taler-wallet-core/src/operations/pay.ts
rename to packages/taler-wallet-core/src/operations/pay-merchant.ts
index 6757b79b4..d590177c2 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -26,14 +26,21 @@
  */
 import { GlobalIDB } from "@gnu-taler/idb-bridge";
 import {
+  AbortingCoin,
+  AbortRequest,
   AbsoluteTime,
   AgeRestriction,
   AmountJson,
   Amounts,
+  ApplyRefundResponse,
+  codecForAbortResponse,
   codecForContractTerms,
+  codecForMerchantOrderRefundPickupResponse,
+  codecForMerchantOrderStatusPaid,
   codecForMerchantPayResponse,
   codecForProposal,
   CoinDepositPermission,
+  CoinPublicKey,
   ConfirmPayResult,
   ConfirmPayResultType,
   ContractTerms,
@@ -46,12 +53,17 @@ import {
   HttpStatusCode,
   j2s,
   Logger,
+  MerchantCoinRefundFailureStatus,
+  MerchantCoinRefundStatus,
+  MerchantCoinRefundSuccessStatus,
   NotificationType,
   parsePaytoUri,
   parsePayUri,
+  parseRefundUri,
   PayCoinSelection,
   PreparePayResult,
   PreparePayResultType,
+  PrepareRefundResult,
   RefreshReason,
   strcmp,
   TalerErrorCode,
@@ -62,17 +74,19 @@ import {
 } from "@gnu-taler/taler-util";
 import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
 import {
-  AbortStatus,
   AllowedAuditorInfo,
   AllowedExchangeInfo,
   BackupProviderStateTag,
   CoinRecord,
   CoinStatus,
   DenominationRecord,
-  ProposalRecord,
-  ProposalStatus,
+  ProposalDownload,
+  PurchaseStatus,
   PurchaseRecord,
+  RefundReason,
+  RefundState,
   WalletContractData,
+  WalletStoresV1,
 } from "../db.js";
 import {
   makeErrorDetail,
@@ -80,6 +94,7 @@ import {
   TalerError,
   TalerProtocolViolationError,
 } from "../errors.js";
+import { GetReadWriteAccess } from "../index.browser.js";
 import {
   EXCHANGE_COINS_LOCK,
   InternalWalletState,
@@ -109,12 +124,12 @@ import {
 } from "../util/retries.js";
 import {
   spendCoins,
-  storeOperationError,
   storeOperationPending,
-} from "../wallet.js";
+  storeOperationError,
+  makeEventId,
+} from "./common.js";
 import { getExchangeDetails } from "./exchanges.js";
-import { getTotalRefreshCost } from "./refresh.js";
-import { makeEventId } from "./transactions.js";
+import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
 
 /**
  * Logger.
@@ -203,98 +218,20 @@ export interface CoinSelectionRequest {
   minimumAge?: number;
 }
 
-/**
- * Record all information that is necessary to
- * pay for a proposal in the wallet's database.
- */
-async function recordConfirmPay(
-  ws: InternalWalletState,
-  proposal: ProposalRecord,
-  coinSelection: PayCoinSelection,
-  coinDepositPermissions: CoinDepositPermission[],
-  sessionIdOverride: string | undefined,
-): Promise<PurchaseRecord> {
-  const d = proposal.download;
-  if (!d) {
-    throw Error("proposal is in invalid state");
-  }
-  let sessionId;
-  if (sessionIdOverride) {
-    sessionId = sessionIdOverride;
-  } else {
-    sessionId = proposal.downloadSessionId;
-  }
-  logger.trace(
-    `recording payment on ${proposal.orderId} with session ID ${sessionId}`,
-  );
-  const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
-  const t: PurchaseRecord = {
-    abortStatus: AbortStatus.None,
-    download: d,
-    lastSessionId: sessionId,
-    payCoinSelection: coinSelection,
-    payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
-    totalPayCost: payCostInfo,
-    coinDepositPermissions,
-    timestampAccept: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
-    timestampLastRefundStatus: undefined,
-    proposalId: proposal.proposalId,
-    refundQueryRequested: false,
-    timestampFirstSuccessfulPay: undefined,
-    autoRefundDeadline: undefined,
-    refundAwaiting: undefined,
-    paymentSubmitPending: true,
-    refunds: {},
-    merchantPaySig: undefined,
-    noncePriv: proposal.noncePriv,
-    noncePub: proposal.noncePub,
-  };
-
-  await ws.db
-    .mktx((x) => [
-      x.proposals,
-      x.purchases,
-      x.coins,
-      x.refreshGroups,
-      x.denominations,
-      x.coinAvailability,
-    ])
-    .runReadWrite(async (tx) => {
-      const p = await tx.proposals.get(proposal.proposalId);
-      if (p) {
-        p.proposalStatus = ProposalStatus.Accepted;
-        await tx.proposals.put(p);
-      }
-      await tx.purchases.put(t);
-      await spendCoins(ws, tx, {
-        allocationId: `proposal:${t.proposalId}`,
-        coinPubs: coinSelection.coinPubs,
-        contributions: coinSelection.coinContributions,
-        refreshReason: RefreshReason.PayMerchant,
-      });
-    });
-
-  ws.notify({
-    type: NotificationType.ProposalAccepted,
-    proposalId: proposal.proposalId,
-  });
-  return t;
-}
-
 async function failProposalPermanently(
   ws: InternalWalletState,
   proposalId: string,
   err: TalerErrorDetail,
 ): Promise<void> {
   await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadWrite(async (tx) => {
-      const p = await tx.proposals.get(proposalId);
+      const p = await tx.purchases.get(proposalId);
       if (!p) {
         return;
       }
-      p.proposalStatus = ProposalStatus.PermanentlyFailed;
-      await tx.proposals.put(p);
+      p.purchaseStatus = PurchaseStatus.ProposalDownloadFailed;
+      await tx.purchases.put(p);
     });
 }
 
@@ -309,10 +246,24 @@ function getProposalRequestTimeout(retryInfo?: 
RetryInfo): Duration {
 function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
   return Duration.multiply(
     { d_ms: 15000 },
-    1 + purchase.payCoinSelection.coinPubs.length / 5,
+    1 + (purchase.payInfo?.payCoinSelection.coinPubs.length ?? 0) / 5,
   );
 }
 
+/**
+ * Return the proposal download data for a purchase, throw if not available.
+ *
+ * (Async since in the future this will query the DB.)
+ */
+export async function expectProposalDownload(
+  p: PurchaseRecord,
+): Promise<ProposalDownload> {
+  if (!p.download) {
+    throw Error("expected proposal to be downloaded");
+  }
+  return p.download;
+}
+
 export function extractContractData(
   parsedContractTerms: ContractTerms,
   contractTermsHash: string,
@@ -366,9 +317,9 @@ export async function processDownloadProposal(
   options: object = {},
 ): Promise<OperationAttemptResult> {
   const proposal = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadOnly(async (tx) => {
-      return await tx.proposals.get(proposalId);
+      return await tx.purchases.get(proposalId);
     });
 
   if (!proposal) {
@@ -378,7 +329,7 @@ export async function processDownloadProposal(
     };
   }
 
-  if (proposal.proposalStatus != ProposalStatus.Downloading) {
+  if (proposal.purchaseStatus != PurchaseStatus.DownloadingProposal) {
     return {
       type: OperationAttemptResultType.Finished,
       result: undefined,
@@ -401,7 +352,7 @@ export async function processDownloadProposal(
     requestBody.token = proposal.claimToken;
   }
 
-  const opId = RetryTags.forProposalClaim(proposal);
+  const opId = RetryTags.forPay(proposal);
   const retryRecord = await ws.db
     .mktx((x) => [x.operationRetries])
     .runReadOnly(async (tx) => {
@@ -543,13 +494,13 @@ export async function processDownloadProposal(
   logger.trace(`extracted contract data: ${j2s(contractData)}`);
 
   await ws.db
-    .mktx((x) => [x.purchases, x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadWrite(async (tx) => {
-      const p = await tx.proposals.get(proposalId);
+      const p = await tx.purchases.get(proposalId);
       if (!p) {
         return;
       }
-      if (p.proposalStatus !== ProposalStatus.Downloading) {
+      if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) {
         return;
       }
       p.download = {
@@ -565,14 +516,14 @@ export async function processDownloadProposal(
           await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
         if (differentPurchase) {
           logger.warn("repurchase detected");
-          p.proposalStatus = ProposalStatus.Repurchase;
+          p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
           p.repurchaseProposalId = differentPurchase.proposalId;
-          await tx.proposals.put(p);
+          await tx.purchases.put(p);
           return;
         }
       }
-      p.proposalStatus = ProposalStatus.Proposed;
-      await tx.proposals.put(p);
+      p.purchaseStatus = PurchaseStatus.Proposed;
+      await tx.purchases.put(p);
     });
 
   ws.notify({
@@ -602,9 +553,9 @@ async function startDownloadProposal(
   noncePriv: string | undefined,
 ): Promise<string> {
   const oldProposal = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadOnly(async (tx) => {
-      return tx.proposals.indexes.byUrlAndOrderId.get([
+      return tx.purchases.indexes.byUrlAndOrderId.get([
         merchantBaseUrl,
         orderId,
       ]);
@@ -635,7 +586,7 @@ async function startDownloadProposal(
   const { priv, pub } = noncePair;
   const proposalId = encodeCrock(getRandomBytes(32));
 
-  const proposalRecord: ProposalRecord = {
+  const proposalRecord: PurchaseRecord = {
     download: undefined,
     noncePriv: priv,
     noncePub: pub,
@@ -644,15 +595,25 @@ async function startDownloadProposal(
     merchantBaseUrl,
     orderId,
     proposalId: proposalId,
-    proposalStatus: ProposalStatus.Downloading,
+    purchaseStatus: PurchaseStatus.DownloadingProposal,
     repurchaseProposalId: undefined,
     downloadSessionId: sessionId,
+    autoRefundDeadline: undefined,
+    lastSessionId: undefined,
+    merchantPaySig: undefined,
+    payInfo: undefined,
+    refundAmountAwaiting: undefined,
+    refunds: {},
+    timestampAccept: undefined,
+    timestampFirstSuccessfulPay: undefined,
+    timestampLastRefundStatus: undefined,
+    pendingRemovedCoinPubs: undefined,
   };
 
   await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadWrite(async (tx) => {
-      const existingRecord = await tx.proposals.indexes.byUrlAndOrderId.get([
+      const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
         merchantBaseUrl,
         orderId,
       ]);
@@ -660,7 +621,7 @@ async function startDownloadProposal(
         // Created concurrently
         return;
       }
-      await tx.proposals.put(proposalRecord);
+      await tx.purchases.put(proposalRecord);
     });
 
   await processDownloadProposal(ws, proposalId);
@@ -688,15 +649,17 @@ async function storeFirstPaySuccess(
         logger.warn("payment success already stored");
         return;
       }
+      if (purchase.purchaseStatus === PurchaseStatus.Paying) {
+        purchase.purchaseStatus = PurchaseStatus.Paid;
+      }
       purchase.timestampFirstSuccessfulPay = now;
-      purchase.paymentSubmitPending = false;
       purchase.lastSessionId = sessionId;
       purchase.merchantPaySig = paySig;
-      const protoAr = purchase.download.contractData.autoRefund;
+      const protoAr = purchase.download!.contractData.autoRefund;
       if (protoAr) {
         const ar = Duration.fromTalerProtocolDuration(protoAr);
         logger.info("auto_refund present");
-        purchase.refundQueryRequested = true;
+        purchase.purchaseStatus = PurchaseStatus.QueryingAutoRefund;
         purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
           AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
         );
@@ -723,7 +686,9 @@ async function storePayReplaySuccess(
       if (isFirst) {
         throw Error("invalid payment state");
       }
-      purchase.paymentSubmitPending = false;
+      if (purchase.purchaseStatus === PurchaseStatus.Paying) {
+        purchase.purchaseStatus = PurchaseStatus.Paid;
+      }
       purchase.lastSessionId = sessionId;
       await tx.purchases.put(purchase);
     });
@@ -774,19 +739,26 @@ async function handleInsufficientFunds(
     throw new TalerProtocolViolationError();
   }
 
-  const { contractData } = proposal.download;
+  const { contractData } = proposal.download!;
 
   const prevPayCoins: PreviousPayCoins = [];
 
+  const payInfo = proposal.payInfo;
+  if (!payInfo) {
+    return;
+  }
+
+  const payCoinSelection = payInfo.payCoinSelection;
+
   await ws.db
     .mktx((x) => [x.coins, x.denominations])
     .runReadOnly(async (tx) => {
-      for (let i = 0; i < proposal.payCoinSelection.coinPubs.length; i++) {
-        const coinPub = proposal.payCoinSelection.coinPubs[i];
+      for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+        const coinPub = payCoinSelection.coinPubs[i];
         if (coinPub === brokenCoinPub) {
           continue;
         }
-        const contrib = proposal.payCoinSelection.coinContributions[i];
+        const contrib = payCoinSelection.coinContributions[i];
         const coin = await tx.coins.get(coinPub);
         if (!coin) {
           continue;
@@ -839,14 +811,19 @@ async function handleInsufficientFunds(
       if (!p) {
         return;
       }
-      p.payCoinSelection = res;
-      p.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
-      p.coinDepositPermissions = undefined;
+      const payInfo = p.payInfo;
+      if (!payInfo) {
+        return;
+      }
+      payInfo.payCoinSelection = res;
+      payInfo.payCoinSelection = res;
+      payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+      payInfo.coinDepositPermissions = undefined;
       await tx.purchases.put(p);
       await spendCoins(ws, tx, {
         allocationId: `proposal:${p.proposalId}`,
-        coinPubs: p.payCoinSelection.coinPubs,
-        contributions: p.payCoinSelection.coinContributions,
+        coinPubs: payInfo.payCoinSelection.coinPubs,
+        contributions: payInfo.payCoinSelection.coinContributions,
         refreshReason: RefreshReason.PayMerchant,
       });
     });
@@ -1255,23 +1232,23 @@ export async function checkPaymentByProposalId(
   sessionId?: string,
 ): Promise<PreparePayResult> {
   let proposal = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadOnly(async (tx) => {
-      return tx.proposals.get(proposalId);
+      return tx.purchases.get(proposalId);
     });
   if (!proposal) {
     throw Error(`could not get proposal ${proposalId}`);
   }
-  if (proposal.proposalStatus === ProposalStatus.Repurchase) {
+  if (proposal.purchaseStatus === PurchaseStatus.RepurchaseDetected) {
     const existingProposalId = proposal.repurchaseProposalId;
     if (!existingProposalId) {
       throw Error("invalid proposal state");
     }
     logger.trace("using existing purchase for same product");
     proposal = await ws.db
-      .mktx((x) => [x.proposals])
+      .mktx((x) => [x.purchases])
       .runReadOnly(async (tx) => {
-        return tx.proposals.get(existingProposalId);
+        return tx.purchases.get(existingProposalId);
       });
     if (!proposal) {
       throw Error("existing proposal is in wrong state");
@@ -1297,7 +1274,7 @@ export async function checkPaymentByProposalId(
       return tx.purchases.get(proposalId);
     });
 
-  if (!purchase) {
+  if (!purchase || purchase.purchaseStatus === PurchaseStatus.Proposed) {
     // If not already paid, check if we could pay for it.
     const res = await selectPayCoinsNew(ws, {
       auditors: contractData.allowedAuditors,
@@ -1337,10 +1314,14 @@ export async function checkPaymentByProposalId(
     };
   }
 
-  if (purchase.lastSessionId !== sessionId) {
+  if (
+    purchase.purchaseStatus === PurchaseStatus.Paid &&
+    purchase.lastSessionId !== sessionId
+  ) {
     logger.trace(
       "automatically re-submitting payment with different session ID",
     );
+    logger.trace(`last: ${purchase.lastSessionId}, current: ${sessionId}`);
     await ws.db
       .mktx((x) => [x.purchases])
       .runReadWrite(async (tx) => {
@@ -1349,7 +1330,7 @@ export async function checkPaymentByProposalId(
           return;
         }
         p.lastSessionId = sessionId;
-        p.paymentSubmitPending = true;
+        p.purchaseStatus = PurchaseStatus.PayingReplay;
         await tx.purchases.put(p);
       });
     const r = await processPurchasePay(ws, proposalId, { forceNow: true });
@@ -1357,35 +1338,41 @@ export async function checkPaymentByProposalId(
       // FIXME: This does not surface the original error
       throw Error("submitting pay failed");
     }
+    const download = await expectProposalDownload(purchase);
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: purchase.download.contractTermsRaw,
-      contractTermsHash: purchase.download.contractData.contractTermsHash,
+      contractTerms: download.contractTermsRaw,
+      contractTermsHash: download.contractData.contractTermsHash,
       paid: true,
-      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
-      amountEffective: Amounts.stringify(purchase.totalPayCost),
+      amountRaw: Amounts.stringify(download.contractData.amount),
+      amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
       proposalId,
     };
   } else if (!purchase.timestampFirstSuccessfulPay) {
+    const download = await expectProposalDownload(purchase);
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: purchase.download.contractTermsRaw,
-      contractTermsHash: purchase.download.contractData.contractTermsHash,
+      contractTerms: download.contractTermsRaw,
+      contractTermsHash: download.contractData.contractTermsHash,
       paid: false,
-      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
-      amountEffective: Amounts.stringify(purchase.totalPayCost),
+      amountRaw: Amounts.stringify(download.contractData.amount),
+      amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
       proposalId,
     };
   } else {
-    const paid = !purchase.paymentSubmitPending;
+    const paid =
+      purchase.purchaseStatus === PurchaseStatus.Paid ||
+      purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
+      purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund;
+    const download = await expectProposalDownload(purchase);
     return {
       status: PreparePayResultType.AlreadyConfirmed,
-      contractTerms: purchase.download.contractTermsRaw,
-      contractTermsHash: purchase.download.contractData.contractTermsHash,
+      contractTerms: download.contractTermsRaw,
+      contractTermsHash: download.contractData.contractTermsHash,
       paid,
-      amountRaw: Amounts.stringify(purchase.download.contractData.amount),
-      amountEffective: Amounts.stringify(purchase.totalPayCost),
-      ...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}),
+      amountRaw: Amounts.stringify(download.contractData.amount),
+      amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
+      ...(paid ? { nextUrl: download.contractData.orderId } : {}),
       proposalId,
     };
   }
@@ -1396,9 +1383,9 @@ export async function getContractTermsDetails(
   proposalId: string,
 ): Promise<WalletContractData> {
   const proposal = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadOnly(async (tx) => {
-      return tx.proposals.get(proposalId);
+      return tx.purchases.get(proposalId);
     });
 
   if (!proposal) {
@@ -1574,7 +1561,10 @@ export async function runPayForConfirmPay(
       }
     }
     case OperationAttemptResultType.Pending:
-      await storeOperationPending(ws, `${PendingTaskType.Pay}:${proposalId}`);
+      await storeOperationPending(
+        ws,
+        `${PendingTaskType.Purchase}:${proposalId}`,
+      );
       return {
         type: ConfirmPayResultType.Pending,
         transactionId: makeEventId(TransactionType.Payment, proposalId),
@@ -1600,9 +1590,9 @@ export async function confirmPay(
     `executing confirmPay with proposalId ${proposalId} and sessionIdOverride 
${sessionIdOverride}`,
   );
   const proposal = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadOnly(async (tx) => {
-      return tx.proposals.get(proposalId);
+      return tx.purchases.get(proposalId);
     });
 
   if (!proposal) {
@@ -1625,13 +1615,15 @@ export async function confirmPay(
       ) {
         logger.trace(`changing session ID to ${sessionIdOverride}`);
         purchase.lastSessionId = sessionIdOverride;
-        purchase.paymentSubmitPending = true;
+        if (purchase.purchaseStatus === PurchaseStatus.Paid) {
+          purchase.purchaseStatus = PurchaseStatus.PayingReplay;
+        }
         await tx.purchases.put(purchase);
       }
       return purchase;
     });
 
-  if (existingPurchase) {
+  if (existingPurchase && existingPurchase.payInfo) {
     logger.trace("confirmPay: submitting payment for existing purchase");
     return runPayForConfirmPay(ws, proposalId);
   }
@@ -1640,9 +1632,9 @@ export async function confirmPay(
 
   const contractData = d.contractData;
 
-  let res: PayCoinSelection | undefined = undefined;
+  let maybeCoinSelection: PayCoinSelection | undefined = undefined;
 
-  res = await selectPayCoinsNew(ws, {
+  maybeCoinSelection = await selectPayCoinsNew(ws, {
     auditors: contractData.allowedAuditors,
     exchanges: contractData.allowedExchanges,
     wireMethod: contractData.wireMethod,
@@ -1655,9 +1647,9 @@ export async function confirmPay(
     forcedSelection: forcedCoinSel,
   });
 
-  logger.trace("coin selection result", res);
+  logger.trace("coin selection result", maybeCoinSelection);
 
-  if (!res) {
+  if (!maybeCoinSelection) {
     // Should not happen, since checkPay should be called first
     // FIXME: Actually, this should be handled gracefully,
     // and the status should be stored in the DB.
@@ -1665,23 +1657,121 @@ export async function confirmPay(
     throw Error("insufficient balance");
   }
 
+  const coinSelection = maybeCoinSelection;
+
   const depositPermissions = await generateDepositPermissions(
     ws,
-    res,
+    coinSelection,
     d.contractData,
   );
 
-  await recordConfirmPay(
-    ws,
-    proposal,
-    res,
-    depositPermissions,
-    sessionIdOverride,
+  const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
+
+  let sessionId: string | undefined;
+  if (sessionIdOverride) {
+    sessionId = sessionIdOverride;
+  } else {
+    sessionId = proposal.downloadSessionId;
+  }
+
+  logger.trace(
+    `recording payment on ${proposal.orderId} with session ID ${sessionId}`,
   );
 
+  await ws.db
+    .mktx((x) => [
+      x.purchases,
+      x.coins,
+      x.refreshGroups,
+      x.denominations,
+      x.coinAvailability,
+    ])
+    .runReadWrite(async (tx) => {
+      const p = await tx.purchases.get(proposal.proposalId);
+      if (!p) {
+        return;
+      }
+      switch (p.purchaseStatus) {
+        case PurchaseStatus.Proposed:
+          p.payInfo = {
+            payCoinSelection: coinSelection,
+            payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
+            totalPayCost: payCostInfo,
+            coinDepositPermissions: depositPermissions,
+          };
+          p.lastSessionId = sessionId;
+          p.timestampAccept = TalerProtocolTimestamp.now();
+          p.purchaseStatus = PurchaseStatus.Paying;
+          await tx.purchases.put(p);
+          await spendCoins(ws, tx, {
+            allocationId: `proposal:${p.proposalId}`,
+            coinPubs: coinSelection.coinPubs,
+            contributions: coinSelection.coinContributions,
+            refreshReason: RefreshReason.PayMerchant,
+          });
+          break;
+        case PurchaseStatus.Paid:
+        case PurchaseStatus.Paying:
+        default:
+          break;
+      }
+    });
+
+  ws.notify({
+    type: NotificationType.ProposalAccepted,
+    proposalId: proposal.proposalId,
+  });
+
   return runPayForConfirmPay(ws, proposalId);
 }
 
+export async function processPurchase(
+  ws: InternalWalletState,
+  proposalId: string,
+  options: {
+    forceNow?: boolean;
+  } = {},
+): Promise<OperationAttemptResult> {
+  const purchase = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadOnly(async (tx) => {
+      return tx.purchases.get(proposalId);
+    });
+  if (!purchase) {
+    return {
+      type: OperationAttemptResultType.Error,
+      errorDetail: {
+        // FIXME: allocate more specific error code
+        code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+        hint: `trying to pay for purchase that is not in the database`,
+        proposalId: proposalId,
+      },
+    };
+  }
+
+  switch (purchase.purchaseStatus) {
+    case PurchaseStatus.DownloadingProposal:
+      return processDownloadProposal(ws, proposalId, options);
+    case PurchaseStatus.Paying:
+    case PurchaseStatus.PayingReplay:
+      return processPurchasePay(ws, proposalId, options);
+    case PurchaseStatus.QueryingRefund:
+    case PurchaseStatus.QueryingAutoRefund:
+    case PurchaseStatus.AbortingWithRefund:
+      return processPurchaseQueryRefund(ws, proposalId, options);
+    case PurchaseStatus.ProposalDownloadFailed:
+    case PurchaseStatus.Paid:
+    case PurchaseStatus.AbortingWithRefund:
+    case PurchaseStatus.RepurchaseDetected:
+      return {
+        type: OperationAttemptResultType.Finished,
+        result: undefined,
+      };
+    default:
+      throw Error(`unexpected purchase status (${purchase.purchaseStatus})`);
+  }
+}
+
 export async function processPurchasePay(
   ws: InternalWalletState,
   proposalId: string,
@@ -1705,31 +1795,38 @@ export async function processPurchasePay(
       },
     };
   }
-  if (!purchase.paymentSubmitPending) {
-    OperationAttemptResult.finishedEmpty();
+  switch (purchase.purchaseStatus) {
+    case PurchaseStatus.Paying:
+    case PurchaseStatus.PayingReplay:
+      break;
+    default:
+      return OperationAttemptResult.finishedEmpty();
   }
   logger.trace(`processing purchase pay ${proposalId}`);
 
   const sessionId = purchase.lastSessionId;
 
-  logger.trace("paying with session ID", sessionId);
+  logger.trace(`paying with session ID ${sessionId}`);
+  const payInfo = purchase.payInfo;
+  checkDbInvariant(!!payInfo, "payInfo");
 
+  const download = await expectProposalDownload(purchase);
   if (!purchase.merchantPaySig) {
     const payUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/pay`,
-      purchase.download.contractData.merchantBaseUrl,
+      `orders/${download.contractData.orderId}/pay`,
+      download.contractData.merchantBaseUrl,
     ).href;
 
     let depositPermissions: CoinDepositPermission[];
 
-    if (purchase.coinDepositPermissions) {
-      depositPermissions = purchase.coinDepositPermissions;
+    if (purchase.payInfo?.coinDepositPermissions) {
+      depositPermissions = purchase.payInfo.coinDepositPermissions;
     } else {
       // FIXME: also cache!
       depositPermissions = await generateDepositPermissions(
         ws,
-        purchase.payCoinSelection,
-        purchase.download.contractData,
+        payInfo.payCoinSelection,
+        download.contractData,
       );
     }
 
@@ -1775,7 +1872,8 @@ export async function processPurchasePay(
           if (!purch) {
             return;
           }
-          purch.payFrozen = true;
+          // FIXME: Should be some "PayPermanentlyFailed" and error info 
should be stored
+          purch.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
           await tx.purchases.put(purch);
         });
       throw makePendingOperationFailedError(
@@ -1819,9 +1917,9 @@ export async function processPurchasePay(
 
     logger.trace("got success from pay URL", merchantResp);
 
-    const merchantPub = purchase.download.contractData.merchantPub;
+    const merchantPub = download.contractData.merchantPub;
     const { valid } = await ws.cryptoApi.isValidPaymentSignature({
-      contractHash: purchase.download.contractData.contractTermsHash,
+      contractHash: download.contractData.contractTermsHash,
       merchantPub,
       sig: merchantResp.sig,
     });
@@ -1836,17 +1934,19 @@ export async function processPurchasePay(
     await unblockBackup(ws, proposalId);
   } else {
     const payAgainUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/paid`,
-      purchase.download.contractData.merchantBaseUrl,
+      `orders/${download.contractData.orderId}/paid`,
+      download.contractData.merchantBaseUrl,
     ).href;
     const reqBody = {
       sig: purchase.merchantPaySig,
-      h_contract: purchase.download.contractData.contractTermsHash,
+      h_contract: download.contractData.contractTermsHash,
       session_id: sessionId ?? "",
     };
+    logger.trace(`/paid request body: ${j2s(reqBody)}`);
     const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
       ws.http.postJson(payAgainUrl, reqBody),
     );
+    logger.trace(`/paid response status: ${resp.status}`);
     if (resp.status !== 204) {
       throw TalerError.fromDetail(
         TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
@@ -1871,18 +1971,18 @@ export async function refuseProposal(
   proposalId: string,
 ): Promise<void> {
   const success = await ws.db
-    .mktx((x) => [x.proposals])
+    .mktx((x) => [x.purchases])
     .runReadWrite(async (tx) => {
-      const proposal = await tx.proposals.get(proposalId);
+      const proposal = await tx.purchases.get(proposalId);
       if (!proposal) {
         logger.trace(`proposal ${proposalId} not found, won't refuse 
proposal`);
         return false;
       }
-      if (proposal.proposalStatus !== ProposalStatus.Proposed) {
+      if (proposal.purchaseStatus !== PurchaseStatus.Proposed) {
         return false;
       }
-      proposal.proposalStatus = ProposalStatus.Refused;
-      await tx.proposals.put(proposal);
+      proposal.purchaseStatus = PurchaseStatus.ProposalRefused;
+      await tx.purchases.put(proposal);
       return true;
     });
   if (success) {
@@ -1891,3 +1991,771 @@ export async function refuseProposal(
     });
   }
 }
+
+export async function prepareRefund(
+  ws: InternalWalletState,
+  talerRefundUri: string,
+): Promise<PrepareRefundResult> {
+  const parseResult = parseRefundUri(talerRefundUri);
+
+  logger.trace("preparing refund offer", parseResult);
+
+  if (!parseResult) {
+    throw Error("invalid refund URI");
+  }
+
+  const purchase = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadOnly(async (tx) => {
+      return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
+        parseResult.merchantBaseUrl,
+        parseResult.orderId,
+      ]);
+    });
+
+  if (!purchase) {
+    throw Error(
+      `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
+    );
+  }
+
+  const awaiting = await queryAndSaveAwaitingRefund(ws, purchase);
+  const summary = await calculateRefundSummary(purchase);
+  const proposalId = purchase.proposalId;
+
+  const { contractData: c } = await expectProposalDownload(purchase);
+
+  return {
+    proposalId,
+    effectivePaid: Amounts.stringify(summary.amountEffectivePaid),
+    gone: Amounts.stringify(summary.amountRefundGone),
+    granted: Amounts.stringify(summary.amountRefundGranted),
+    pending: summary.pendingAtExchange,
+    awaiting: Amounts.stringify(awaiting),
+    info: {
+      contractTermsHash: c.contractTermsHash,
+      merchant: c.merchant,
+      orderId: c.orderId,
+      products: c.products,
+      summary: c.summary,
+      fulfillmentMessage: c.fulfillmentMessage,
+      summary_i18n: c.summaryI18n,
+      fulfillmentMessage_i18n: c.fulfillmentMessageI18n,
+    },
+  };
+}
+
+function getRefundKey(d: MerchantCoinRefundStatus): string {
+  return `${d.coin_pub}-${d.rtransaction_id}`;
+}
+
+async function applySuccessfulRefund(
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  p: PurchaseRecord,
+  refreshCoinsMap: Record<string, { coinPub: string }>,
+  r: MerchantCoinRefundSuccessStatus,
+): Promise<void> {
+  // FIXME: check signature before storing it as valid!
+
+  const refundKey = getRefundKey(r);
+  const coin = await tx.coins.get(r.coin_pub);
+  if (!coin) {
+    logger.warn("coin not found, can't apply refund");
+    return;
+  }
+  const denom = await tx.denominations.get([
+    coin.exchangeBaseUrl,
+    coin.denomPubHash,
+  ]);
+  if (!denom) {
+    throw Error("inconsistent database");
+  }
+  refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
+  const refundAmount = Amounts.parseOrThrow(r.refund_amount);
+  const refundFee = denom.fees.feeRefund;
+  coin.status = CoinStatus.Dormant;
+  coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
+  coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
+  logger.trace(`coin amount after is 
${Amounts.stringify(coin.currentAmount)}`);
+  await tx.coins.put(coin);
+
+  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
+    .iter(coin.exchangeBaseUrl)
+    .toArray();
+
+  const amountLeft = Amounts.sub(
+    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+      .amount,
+    denom.fees.feeRefund,
+  ).amount;
+
+  const totalRefreshCostBound = getTotalRefreshCost(
+    allDenoms,
+    DenominationRecord.toDenomInfo(denom),
+    amountLeft,
+  );
+
+  p.refunds[refundKey] = {
+    type: RefundState.Applied,
+    obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
+    executionTime: r.execution_time,
+    refundAmount: Amounts.parseOrThrow(r.refund_amount),
+    refundFee: denom.fees.feeRefund,
+    totalRefreshCostBound,
+    coinPub: r.coin_pub,
+    rtransactionId: r.rtransaction_id,
+  };
+}
+
+async function storePendingRefund(
+  tx: GetReadWriteAccess<{
+    denominations: typeof WalletStoresV1.denominations;
+    coins: typeof WalletStoresV1.coins;
+  }>,
+  p: PurchaseRecord,
+  r: MerchantCoinRefundFailureStatus,
+): Promise<void> {
+  const refundKey = getRefundKey(r);
+
+  const coin = await tx.coins.get(r.coin_pub);
+  if (!coin) {
+    logger.warn("coin not found, can't apply refund");
+    return;
+  }
+  const denom = await tx.denominations.get([
+    coin.exchangeBaseUrl,
+    coin.denomPubHash,
+  ]);
+
+  if (!denom) {
+    throw Error("inconsistent database");
+  }
+
+  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
+    .iter(coin.exchangeBaseUrl)
+    .toArray();
+
+  const amountLeft = Amounts.sub(
+    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+      .amount,
+    denom.fees.feeRefund,
+  ).amount;
+
+  const totalRefreshCostBound = getTotalRefreshCost(
+    allDenoms,
+    DenominationRecord.toDenomInfo(denom),
+    amountLeft,
+  );
+
+  p.refunds[refundKey] = {
+    type: RefundState.Pending,
+    obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
+    executionTime: r.execution_time,
+    refundAmount: Amounts.parseOrThrow(r.refund_amount),
+    refundFee: denom.fees.feeRefund,
+    totalRefreshCostBound,
+    coinPub: r.coin_pub,
+    rtransactionId: r.rtransaction_id,
+  };
+}
+
+async function storeFailedRefund(
+  tx: GetReadWriteAccess<{
+    coins: typeof WalletStoresV1.coins;
+    denominations: typeof WalletStoresV1.denominations;
+  }>,
+  p: PurchaseRecord,
+  refreshCoinsMap: Record<string, { coinPub: string }>,
+  r: MerchantCoinRefundFailureStatus,
+): Promise<void> {
+  const refundKey = getRefundKey(r);
+
+  const coin = await tx.coins.get(r.coin_pub);
+  if (!coin) {
+    logger.warn("coin not found, can't apply refund");
+    return;
+  }
+  const denom = await tx.denominations.get([
+    coin.exchangeBaseUrl,
+    coin.denomPubHash,
+  ]);
+
+  if (!denom) {
+    throw Error("inconsistent database");
+  }
+
+  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
+    .iter(coin.exchangeBaseUrl)
+    .toArray();
+
+  const amountLeft = Amounts.sub(
+    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+      .amount,
+    denom.fees.feeRefund,
+  ).amount;
+
+  const totalRefreshCostBound = getTotalRefreshCost(
+    allDenoms,
+    DenominationRecord.toDenomInfo(denom),
+    amountLeft,
+  );
+
+  p.refunds[refundKey] = {
+    type: RefundState.Failed,
+    obtainedTime: TalerProtocolTimestamp.now(),
+    executionTime: r.execution_time,
+    refundAmount: Amounts.parseOrThrow(r.refund_amount),
+    refundFee: denom.fees.feeRefund,
+    totalRefreshCostBound,
+    coinPub: r.coin_pub,
+    rtransactionId: r.rtransaction_id,
+  };
+
+  if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
+    // Refund failed because the merchant didn't even try to deposit
+    // the coin yet, so we try to refresh.
+    if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
+      const coin = await tx.coins.get(r.coin_pub);
+      if (!coin) {
+        logger.warn("coin not found, can't apply refund");
+        return;
+      }
+      const denom = await tx.denominations.get([
+        coin.exchangeBaseUrl,
+        coin.denomPubHash,
+      ]);
+      if (!denom) {
+        logger.warn("denomination for coin missing");
+        return;
+      }
+      const payCoinSelection = p.payInfo?.payCoinSelection;
+      if (!payCoinSelection) {
+        logger.warn("no pay coin selection, can't apply refund");
+        return;
+      }
+      let contrib: AmountJson | undefined;
+      for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+        if (payCoinSelection.coinPubs[i] === r.coin_pub) {
+          contrib = payCoinSelection.coinContributions[i];
+        }
+      }
+      if (contrib) {
+        coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount;
+        coin.currentAmount = Amounts.sub(
+          coin.currentAmount,
+          denom.fees.feeRefund,
+        ).amount;
+      }
+      refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
+      await tx.coins.put(coin);
+    }
+  }
+}
+
+async function acceptRefunds(
+  ws: InternalWalletState,
+  proposalId: string,
+  refunds: MerchantCoinRefundStatus[],
+  reason: RefundReason,
+): Promise<void> {
+  logger.trace("handling refunds", refunds);
+  const now = TalerProtocolTimestamp.now();
+
+  await ws.db
+    .mktx((x) => [
+      x.purchases,
+      x.coins,
+      x.coinAvailability,
+      x.denominations,
+      x.refreshGroups,
+    ])
+    .runReadWrite(async (tx) => {
+      const p = await tx.purchases.get(proposalId);
+      if (!p) {
+        logger.error("purchase not found, not adding refunds");
+        return;
+      }
+
+      const refreshCoinsMap: Record<string, CoinPublicKey> = {};
+
+      for (const refundStatus of refunds) {
+        const refundKey = getRefundKey(refundStatus);
+        const existingRefundInfo = p.refunds[refundKey];
+
+        const isPermanentFailure =
+          refundStatus.type === "failure" &&
+          refundStatus.exchange_status >= 400 &&
+          refundStatus.exchange_status < 500;
+
+        // Already failed.
+        if (existingRefundInfo?.type === RefundState.Failed) {
+          continue;
+        }
+
+        // Already applied.
+        if (existingRefundInfo?.type === RefundState.Applied) {
+          continue;
+        }
+
+        // Still pending.
+        if (
+          refundStatus.type === "failure" &&
+          !isPermanentFailure &&
+          existingRefundInfo?.type === RefundState.Pending
+        ) {
+          continue;
+        }
+
+        // Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
+
+        if (refundStatus.type === "success") {
+          await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
+        } else if (isPermanentFailure) {
+          await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
+        } else {
+          await storePendingRefund(tx, p, refundStatus);
+        }
+      }
+
+      const refreshCoinsPubs = Object.values(refreshCoinsMap);
+      if (refreshCoinsPubs.length > 0) {
+        await createRefreshGroup(
+          ws,
+          tx,
+          refreshCoinsPubs,
+          RefreshReason.Refund,
+        );
+      }
+
+      // Are we done with querying yet, or do we need to do another round
+      // after a retry delay?
+      let queryDone = true;
+
+      let numPendingRefunds = 0;
+      for (const ri of Object.values(p.refunds)) {
+        switch (ri.type) {
+          case RefundState.Pending:
+            numPendingRefunds++;
+            break;
+        }
+      }
+
+      if (numPendingRefunds > 0) {
+        queryDone = false;
+      }
+
+      if (queryDone) {
+        p.timestampLastRefundStatus = now;
+        if (p.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
+          p.purchaseStatus = PurchaseStatus.PaymentAbortFinished;
+        } else if (p.purchaseStatus === PurchaseStatus.QueryingAutoRefund) {
+          const autoRefundDeadline = p.autoRefundDeadline;
+          checkDbInvariant(!!autoRefundDeadline);
+          if (
+            AbsoluteTime.isExpired(
+              AbsoluteTime.fromTimestamp(autoRefundDeadline),
+            )
+          ) {
+            p.purchaseStatus = PurchaseStatus.Paid;
+          }
+        } else if (p.purchaseStatus === PurchaseStatus.QueryingRefund) {
+          p.purchaseStatus = PurchaseStatus.Paid;
+        }
+        logger.trace("refund query done");
+      } else {
+        // No error, but we need to try again!
+        p.timestampLastRefundStatus = now;
+        logger.trace("refund query not done");
+      }
+
+      await tx.purchases.put(p);
+    });
+
+  ws.notify({
+    type: NotificationType.RefundQueried,
+  });
+}
+
+async function calculateRefundSummary(
+  p: PurchaseRecord,
+): Promise<RefundSummary> {
+  const download = await expectProposalDownload(p);
+  let amountRefundGranted = Amounts.getZero(
+    download.contractData.amount.currency,
+  );
+  let amountRefundGone = 
Amounts.getZero(download.contractData.amount.currency);
+
+  let pendingAtExchange = false;
+
+  const payInfo = p.payInfo;
+  if (!payInfo) {
+    throw Error("can't calculate refund summary without payInfo");
+  }
+
+  Object.keys(p.refunds).forEach((rk) => {
+    const refund = p.refunds[rk];
+    if (refund.type === RefundState.Pending) {
+      pendingAtExchange = true;
+    }
+    if (
+      refund.type === RefundState.Applied ||
+      refund.type === RefundState.Pending
+    ) {
+      amountRefundGranted = Amounts.add(
+        amountRefundGranted,
+        Amounts.sub(
+          refund.refundAmount,
+          refund.refundFee,
+          refund.totalRefreshCostBound,
+        ).amount,
+      ).amount;
+    } else {
+      amountRefundGone = Amounts.add(
+        amountRefundGone,
+        refund.refundAmount,
+      ).amount;
+    }
+  });
+  return {
+    amountEffectivePaid: payInfo.totalPayCost,
+    amountRefundGone,
+    amountRefundGranted,
+    pendingAtExchange,
+  };
+}
+
+/**
+ * Summary of the refund status of a purchase.
+ */
+export interface RefundSummary {
+  pendingAtExchange: boolean;
+  amountEffectivePaid: AmountJson;
+  amountRefundGranted: AmountJson;
+  amountRefundGone: AmountJson;
+}
+
+/**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+export async function applyRefund(
+  ws: InternalWalletState,
+  talerRefundUri: string,
+): Promise<ApplyRefundResponse> {
+  const parseResult = parseRefundUri(talerRefundUri);
+
+  logger.trace("applying refund", parseResult);
+
+  if (!parseResult) {
+    throw Error("invalid refund URI");
+  }
+
+  const purchase = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadOnly(async (tx) => {
+      return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
+        parseResult.merchantBaseUrl,
+        parseResult.orderId,
+      ]);
+    });
+
+  if (!purchase) {
+    throw Error(
+      `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
+    );
+  }
+
+  return applyRefundFromPurchaseId(ws, purchase.proposalId);
+}
+
+export async function applyRefundFromPurchaseId(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<ApplyRefundResponse> {
+  logger.trace("applying refund for purchase", proposalId);
+
+  logger.info("processing purchase for refund");
+  const success = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadWrite(async (tx) => {
+      const p = await tx.purchases.get(proposalId);
+      if (!p) {
+        logger.error("no purchase found for refund URL");
+        return false;
+      }
+      if (p.purchaseStatus === PurchaseStatus.Paid) {
+        p.purchaseStatus = PurchaseStatus.QueryingRefund;
+      }
+      await tx.purchases.put(p);
+      return true;
+    });
+
+  if (success) {
+    ws.notify({
+      type: NotificationType.RefundStarted,
+    });
+    await processPurchaseQueryRefund(ws, proposalId, {
+      forceNow: true,
+      waitForAutoRefund: false,
+    });
+  }
+
+  const purchase = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadOnly(async (tx) => {
+      return tx.purchases.get(proposalId);
+    });
+
+  if (!purchase) {
+    throw Error("purchase no longer exists");
+  }
+
+  const summary = await calculateRefundSummary(purchase);
+  const download = await expectProposalDownload(purchase);
+
+  return {
+    contractTermsHash: download.contractData.contractTermsHash,
+    proposalId: purchase.proposalId,
+    transactionId: makeEventId(TransactionType.Payment, proposalId), //FIXME: 
can we have the tx id of the refund
+    amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
+    amountRefundGone: Amounts.stringify(summary.amountRefundGone),
+    amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
+    pendingAtExchange: summary.pendingAtExchange,
+    info: {
+      contractTermsHash: download.contractData.contractTermsHash,
+      merchant: download.contractData.merchant,
+      orderId: download.contractData.orderId,
+      products: download.contractData.products,
+      summary: download.contractData.summary,
+      fulfillmentMessage: download.contractData.fulfillmentMessage,
+      summary_i18n: download.contractData.summaryI18n,
+      fulfillmentMessage_i18n: download.contractData.fulfillmentMessageI18n,
+    },
+  };
+}
+
+async function queryAndSaveAwaitingRefund(
+  ws: InternalWalletState,
+  purchase: PurchaseRecord,
+  waitForAutoRefund?: boolean,
+): Promise<AmountJson> {
+  const download = await expectProposalDownload(purchase);
+  const requestUrl = new URL(
+    `orders/${download.contractData.orderId}`,
+    download.contractData.merchantBaseUrl,
+  );
+  requestUrl.searchParams.set(
+    "h_contract",
+    download.contractData.contractTermsHash,
+  );
+  // Long-poll for one second
+  if (waitForAutoRefund) {
+    requestUrl.searchParams.set("timeout_ms", "1000");
+    requestUrl.searchParams.set("await_refund_obtained", "yes");
+    logger.trace("making long-polling request for auto-refund");
+  }
+  const resp = await ws.http.get(requestUrl.href);
+  const orderStatus = await readSuccessResponseJsonOrThrow(
+    resp,
+    codecForMerchantOrderStatusPaid(),
+  );
+  if (!orderStatus.refunded) {
+    // Wait for retry ...
+    return Amounts.getZero(download.contractData.amount.currency);
+  }
+
+  const refundAwaiting = Amounts.sub(
+    Amounts.parseOrThrow(orderStatus.refund_amount),
+    Amounts.parseOrThrow(orderStatus.refund_taken),
+  ).amount;
+
+  if (
+    purchase.refundAmountAwaiting === undefined ||
+    Amounts.cmp(refundAwaiting, purchase.refundAmountAwaiting) !== 0
+  ) {
+    await ws.db
+      .mktx((x) => [x.purchases])
+      .runReadWrite(async (tx) => {
+        const p = await tx.purchases.get(purchase.proposalId);
+        if (!p) {
+          logger.warn("purchase does not exist anymore");
+          return;
+        }
+        p.refundAmountAwaiting = refundAwaiting;
+        await tx.purchases.put(p);
+      });
+  }
+
+  return refundAwaiting;
+}
+
+export async function processPurchaseQueryRefund(
+  ws: InternalWalletState,
+  proposalId: string,
+  options: {
+    forceNow?: boolean;
+    waitForAutoRefund?: boolean;
+  } = {},
+): Promise<OperationAttemptResult> {
+  logger.trace(`processing refund query for proposal ${proposalId}`);
+  const waitForAutoRefund = options.waitForAutoRefund ?? false;
+  const purchase = await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadOnly(async (tx) => {
+      return tx.purchases.get(proposalId);
+    });
+  if (!purchase) {
+    return OperationAttemptResult.finishedEmpty();
+  }
+
+  if (
+    !(
+      purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund ||
+      purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
+      purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund
+    )
+  ) {
+    return OperationAttemptResult.finishedEmpty();
+  }
+
+  const download = await expectProposalDownload(purchase);
+
+  if (purchase.timestampFirstSuccessfulPay) {
+    if (
+      !purchase.autoRefundDeadline ||
+      !AbsoluteTime.isExpired(
+        AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline),
+      )
+    ) {
+      const awaitingAmount = await queryAndSaveAwaitingRefund(
+        ws,
+        purchase,
+        waitForAutoRefund,
+      );
+      if (Amounts.isZero(awaitingAmount)) {
+        return OperationAttemptResult.finishedEmpty();
+      }
+    }
+
+    const requestUrl = new URL(
+      `orders/${download.contractData.orderId}/refund`,
+      download.contractData.merchantBaseUrl,
+    );
+
+    logger.trace(`making refund request to ${requestUrl.href}`);
+
+    const request = await ws.http.postJson(requestUrl.href, {
+      h_contract: download.contractData.contractTermsHash,
+    });
+
+    const refundResponse = await readSuccessResponseJsonOrThrow(
+      request,
+      codecForMerchantOrderRefundPickupResponse(),
+    );
+
+    await acceptRefunds(
+      ws,
+      proposalId,
+      refundResponse.refunds,
+      RefundReason.NormalRefund,
+    );
+  } else if (purchase.purchaseStatus === PurchaseStatus.AbortingWithRefund) {
+    const requestUrl = new URL(
+      `orders/${download.contractData.orderId}/abort`,
+      download.contractData.merchantBaseUrl,
+    );
+
+    const abortingCoins: AbortingCoin[] = [];
+
+    const payCoinSelection = purchase.payInfo?.payCoinSelection;
+    if (!payCoinSelection) {
+      throw Error("can't abort, no coins selected");
+    }
+
+    await ws.db
+      .mktx((x) => [x.coins])
+      .runReadOnly(async (tx) => {
+        for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
+          const coinPub = payCoinSelection.coinPubs[i];
+          const coin = await tx.coins.get(coinPub);
+          checkDbInvariant(!!coin, "expected coin to be present");
+          abortingCoins.push({
+            coin_pub: coinPub,
+            contribution: Amounts.stringify(
+              payCoinSelection.coinContributions[i],
+            ),
+            exchange_url: coin.exchangeBaseUrl,
+          });
+        }
+      });
+
+    const abortReq: AbortRequest = {
+      h_contract: download.contractData.contractTermsHash,
+      coins: abortingCoins,
+    };
+
+    logger.trace(`making order abort request to ${requestUrl.href}`);
+
+    const request = await ws.http.postJson(requestUrl.href, abortReq);
+    const abortResp = await readSuccessResponseJsonOrThrow(
+      request,
+      codecForAbortResponse(),
+    );
+
+    const refunds: MerchantCoinRefundStatus[] = [];
+
+    if (abortResp.refunds.length != abortingCoins.length) {
+      // FIXME: define error code!
+      throw Error("invalid order abort response");
+    }
+
+    for (let i = 0; i < abortResp.refunds.length; i++) {
+      const r = abortResp.refunds[i];
+      refunds.push({
+        ...r,
+        coin_pub: payCoinSelection.coinPubs[i],
+        refund_amount: 
Amounts.stringify(payCoinSelection.coinContributions[i]),
+        rtransaction_id: 0,
+        execution_time: AbsoluteTime.toTimestamp(
+          AbsoluteTime.addDuration(
+            AbsoluteTime.fromTimestamp(download.contractData.timestamp),
+            Duration.fromSpec({ seconds: 1 }),
+          ),
+        ),
+      });
+    }
+    await acceptRefunds(ws, proposalId, refunds, RefundReason.AbortRefund);
+  }
+  return OperationAttemptResult.finishedEmpty();
+}
+
+export async function abortFailedPayWithRefund(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  await ws.db
+    .mktx((x) => [x.purchases])
+    .runReadWrite(async (tx) => {
+      const purchase = await tx.purchases.get(proposalId);
+      if (!purchase) {
+        throw Error("purchase not found");
+      }
+      if (purchase.timestampFirstSuccessfulPay) {
+        // No point in aborting it.  We don't even report an error.
+        logger.warn(`tried to abort successful payment`);
+        return;
+      }
+      if (purchase.purchaseStatus === PurchaseStatus.Paying) {
+        purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund;
+      }
+      await tx.purchases.put(purchase);
+    });
+  processPurchaseQueryRefund(ws, proposalId, {
+    forceNow: true,
+  }).catch((e) => {
+    logger.trace(`error during refund processing after abort pay: ${e}`);
+  });
+}
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts 
b/packages/taler-wallet-core/src/operations/pay-peer.ts
similarity index 99%
rename from packages/taler-wallet-core/src/operations/peer-to-peer.ts
rename to packages/taler-wallet-core/src/operations/pay-peer.ts
index d30cb294d..e9185a9d4 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer.ts
@@ -73,9 +73,8 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadOnlyAccess } from "../util/query.js";
-import { spendCoins } from "../wallet.js";
+import { spendCoins, makeEventId } from "../operations/common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
-import { makeEventId } from "./transactions.js";
 import { internalCreateWithdrawalGroup } from "./withdraw.js";
 
 const logger = new Logger("operations/peer-to-peer.ts");
diff --git a/packages/taler-wallet-core/src/operations/pending.ts 
b/packages/taler-wallet-core/src/operations/pending.ts
index e4c270d85..285cef534 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -22,8 +22,7 @@
  * Imports.
  */
 import {
-  ProposalStatus,
-  AbortStatus,
+  PurchaseStatus,
   WalletStoresV1,
   BackupProviderStateTag,
   RefreshCoinStatus,
@@ -38,7 +37,6 @@ import { AbsoluteTime } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { GetReadOnlyAccess } from "../util/query.js";
 import { RetryTags } from "../util/retries.js";
-import { Wallet } from "../wallet.js";
 import { GlobalIDB } from "@gnu-taler/idb-bridge";
 
 function getPendingCommon(
@@ -184,38 +182,6 @@ async function gatherWithdrawalPending(
   }
 }
 
-async function gatherProposalPending(
-  ws: InternalWalletState,
-  tx: GetReadOnlyAccess<{
-    proposals: typeof WalletStoresV1.proposals;
-    operationRetries: typeof WalletStoresV1.operationRetries;
-  }>,
-  now: AbsoluteTime,
-  resp: PendingOperationsResponse,
-): Promise<void> {
-  await tx.proposals.iter().forEachAsync(async (proposal) => {
-    if (proposal.proposalStatus == ProposalStatus.Proposed) {
-      // Nothing to do, user needs to choose.
-    } else if (proposal.proposalStatus == ProposalStatus.Downloading) {
-      const opId = RetryTags.forProposalClaim(proposal);
-      const retryRecord = await tx.operationRetries.get(opId);
-      const timestampDue =
-        retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now();
-      resp.pendingOperations.push({
-        type: PendingTaskType.ProposalDownload,
-        ...getPendingCommon(ws, opId, timestampDue),
-        givesLifeness: true,
-        merchantBaseUrl: proposal.merchantBaseUrl,
-        orderId: proposal.orderId,
-        proposalId: proposal.proposalId,
-        proposalTimestamp: proposal.timestamp,
-        lastError: retryRecord?.lastError,
-        retryInfo: retryRecord?.retryInfo,
-      });
-    }
-  });
-}
-
 async function gatherDepositPending(
   ws: InternalWalletState,
   tx: GetReadOnlyAccess<{
@@ -286,45 +252,27 @@ async function gatherPurchasePending(
   now: AbsoluteTime,
   resp: PendingOperationsResponse,
 ): Promise<void> {
-  // FIXME: Only iter purchases with some "active" flag!
-  await tx.purchases.iter().forEachAsync(async (pr) => {
-    if (
-      pr.paymentSubmitPending &&
-      pr.abortStatus === AbortStatus.None &&
-      !pr.payFrozen
-    ) {
-      const payOpId = RetryTags.forPay(pr);
-      const payRetryRecord = await tx.operationRetries.get(payOpId);
-
-      const timestampDue =
-        payRetryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
-      resp.pendingOperations.push({
-        type: PendingTaskType.Pay,
-        ...getPendingCommon(ws, payOpId, timestampDue),
-        givesLifeness: true,
-        isReplay: false,
-        proposalId: pr.proposalId,
-        retryInfo: payRetryRecord?.retryInfo,
-        lastError: payRetryRecord?.lastError,
-      });
-    }
-    if (pr.refundQueryRequested) {
-      const refundQueryOpId = RetryTags.forRefundQuery(pr);
-      const refundQueryRetryRecord = await tx.operationRetries.get(
-        refundQueryOpId,
-      );
+  const keyRange = GlobalIDB.KeyRange.bound(
+    OperationStatusRange.ACTIVE_START,
+    OperationStatusRange.ACTIVE_END,
+  );
+  await tx.purchases.indexes.byStatus
+    .iter(keyRange)
+    .forEachAsync(async (pr) => {
+      const opId = RetryTags.forPay(pr);
+      const retryRecord = await tx.operationRetries.get(opId);
       const timestampDue =
-        refundQueryRetryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
+        retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
       resp.pendingOperations.push({
-        type: PendingTaskType.RefundQuery,
-        ...getPendingCommon(ws, refundQueryOpId, timestampDue),
+        type: PendingTaskType.Purchase,
+        ...getPendingCommon(ws, opId, timestampDue),
         givesLifeness: true,
+        statusStr: PurchaseStatus[pr.purchaseStatus],
         proposalId: pr.proposalId,
-        retryInfo: refundQueryRetryRecord?.retryInfo,
-        lastError: refundQueryRetryRecord?.lastError,
+        retryInfo: retryRecord?.retryInfo,
+        lastError: retryRecord?.lastError,
       });
-    }
-  });
+    });
 }
 
 async function gatherRecoupPending(
@@ -404,7 +352,6 @@ export async function getPendingOperations(
       x.refreshGroups,
       x.coins,
       x.withdrawalGroups,
-      x.proposals,
       x.tips,
       x.purchases,
       x.planchets,
@@ -419,7 +366,6 @@ export async function getPendingOperations(
       await gatherExchangePending(ws, tx, now, resp);
       await gatherRefreshPending(ws, tx, now, resp);
       await gatherWithdrawalPending(ws, tx, now, resp);
-      await gatherProposalPending(ws, tx, now, resp);
       await gatherDepositPending(ws, tx, now, resp);
       await gatherTipPending(ws, tx, now, resp);
       await gatherPurchasePending(ws, tx, now, resp);
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 2d92ff8ba..ff6bb4efc 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -27,16 +27,15 @@
 import {
   Amounts,
   codecForRecoupConfirmation,
+  codecForReserveStatus,
   encodeCrock,
   getRandomBytes,
   j2s,
   Logger,
   NotificationType,
   RefreshReason,
-  TalerErrorDetail,
   TalerProtocolTimestamp,
   URL,
-  codecForReserveStatus,
 } from "@gnu-taler/taler-util";
 import {
   CoinRecord,
@@ -44,8 +43,8 @@ import {
   CoinStatus,
   RecoupGroupRecord,
   RefreshCoinSource,
-  WithdrawalGroupStatus,
   WalletStoresV1,
+  WithdrawalGroupStatus,
   WithdrawalRecordType,
   WithdrawCoinSource,
 } from "../db.js";
@@ -54,10 +53,8 @@ import { readSuccessResponseJsonOrThrow } from 
"../util/http.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import {
   OperationAttemptResult,
-  RetryInfo,
   runOperationHandlerForResult,
 } from "../util/retries.js";
-import { guardOperationException } from "./common.js";
 import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
 import { internalCreateWithdrawalGroup } from "./withdraw.js";
 
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 9fe2e6a8f..a5951ea53 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -78,7 +78,7 @@ import {
   OperationAttemptResult,
   OperationAttemptResultType,
 } from "../util/retries.js";
-import { makeCoinAvailable } from "../wallet.js";
+import { makeCoinAvailable } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
   isWithdrawableDenom,
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
deleted file mode 100644
index 0d86b92ab..000000000
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ /dev/null
@@ -1,815 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019-2019 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Implementation of the refund operation.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import {
-  AbortingCoin,
-  AbortRequest,
-  AbsoluteTime,
-  AmountJson,
-  Amounts,
-  ApplyRefundResponse,
-  codecForAbortResponse,
-  codecForMerchantOrderRefundPickupResponse,
-  codecForMerchantOrderStatusPaid,
-  CoinPublicKey,
-  Duration,
-  Logger,
-  MerchantCoinRefundFailureStatus,
-  MerchantCoinRefundStatus,
-  MerchantCoinRefundSuccessStatus,
-  NotificationType,
-  parseRefundUri,
-  PrepareRefundResult,
-  RefreshReason,
-  TalerErrorCode,
-  TalerProtocolTimestamp,
-  TransactionType,
-  URL,
-} from "@gnu-taler/taler-util";
-import {
-  AbortStatus,
-  CoinStatus,
-  DenominationRecord,
-  PurchaseRecord,
-  RefundReason,
-  RefundState,
-  WalletStoresV1,
-} from "../db.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "../util/http.js";
-import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadWriteAccess } from "../util/query.js";
-import { OperationAttemptResult } from "../util/retries.js";
-import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
-import { makeEventId } from "./transactions.js";
-
-const logger = new Logger("refund.ts");
-
-export async function prepareRefund(
-  ws: InternalWalletState,
-  talerRefundUri: string,
-): Promise<PrepareRefundResult> {
-  const parseResult = parseRefundUri(talerRefundUri);
-
-  logger.trace("preparing refund offer", parseResult);
-
-  if (!parseResult) {
-    throw Error("invalid refund URI");
-  }
-
-  const purchase = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadOnly(async (tx) => {
-      return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
-        parseResult.merchantBaseUrl,
-        parseResult.orderId,
-      ]);
-    });
-
-  if (!purchase) {
-    throw Error(
-      `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
-    );
-  }
-
-  const awaiting = await queryAndSaveAwaitingRefund(ws, purchase);
-  const summary = calculateRefundSummary(purchase);
-  const proposalId = purchase.proposalId;
-
-  const { contractData: c } = purchase.download;
-
-  return {
-    proposalId,
-    effectivePaid: Amounts.stringify(summary.amountEffectivePaid),
-    gone: Amounts.stringify(summary.amountRefundGone),
-    granted: Amounts.stringify(summary.amountRefundGranted),
-    pending: summary.pendingAtExchange,
-    awaiting: Amounts.stringify(awaiting),
-    info: {
-      contractTermsHash: c.contractTermsHash,
-      merchant: c.merchant,
-      orderId: c.orderId,
-      products: c.products,
-      summary: c.summary,
-      fulfillmentMessage: c.fulfillmentMessage,
-      summary_i18n: c.summaryI18n,
-      fulfillmentMessage_i18n: c.fulfillmentMessageI18n,
-    },
-  };
-}
-
-function getRefundKey(d: MerchantCoinRefundStatus): string {
-  return `${d.coin_pub}-${d.rtransaction_id}`;
-}
-
-async function applySuccessfulRefund(
-  tx: GetReadWriteAccess<{
-    coins: typeof WalletStoresV1.coins;
-    denominations: typeof WalletStoresV1.denominations;
-  }>,
-  p: PurchaseRecord,
-  refreshCoinsMap: Record<string, { coinPub: string }>,
-  r: MerchantCoinRefundSuccessStatus,
-): Promise<void> {
-  // FIXME: check signature before storing it as valid!
-
-  const refundKey = getRefundKey(r);
-  const coin = await tx.coins.get(r.coin_pub);
-  if (!coin) {
-    logger.warn("coin not found, can't apply refund");
-    return;
-  }
-  const denom = await tx.denominations.get([
-    coin.exchangeBaseUrl,
-    coin.denomPubHash,
-  ]);
-  if (!denom) {
-    throw Error("inconsistent database");
-  }
-  refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
-  const refundAmount = Amounts.parseOrThrow(r.refund_amount);
-  const refundFee = denom.fees.feeRefund;
-  coin.status = CoinStatus.Dormant;
-  coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
-  coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
-  logger.trace(`coin amount after is 
${Amounts.stringify(coin.currentAmount)}`);
-  await tx.coins.put(coin);
-
-  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
-    .iter(coin.exchangeBaseUrl)
-    .toArray();
-
-  const amountLeft = Amounts.sub(
-    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
-      .amount,
-    denom.fees.feeRefund,
-  ).amount;
-
-  const totalRefreshCostBound = getTotalRefreshCost(
-    allDenoms,
-    DenominationRecord.toDenomInfo(denom),
-    amountLeft,
-  );
-
-  p.refunds[refundKey] = {
-    type: RefundState.Applied,
-    obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
-    executionTime: r.execution_time,
-    refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.fees.feeRefund,
-    totalRefreshCostBound,
-    coinPub: r.coin_pub,
-    rtransactionId: r.rtransaction_id,
-  };
-}
-
-async function storePendingRefund(
-  tx: GetReadWriteAccess<{
-    denominations: typeof WalletStoresV1.denominations;
-    coins: typeof WalletStoresV1.coins;
-  }>,
-  p: PurchaseRecord,
-  r: MerchantCoinRefundFailureStatus,
-): Promise<void> {
-  const refundKey = getRefundKey(r);
-
-  const coin = await tx.coins.get(r.coin_pub);
-  if (!coin) {
-    logger.warn("coin not found, can't apply refund");
-    return;
-  }
-  const denom = await tx.denominations.get([
-    coin.exchangeBaseUrl,
-    coin.denomPubHash,
-  ]);
-
-  if (!denom) {
-    throw Error("inconsistent database");
-  }
-
-  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
-    .iter(coin.exchangeBaseUrl)
-    .toArray();
-
-  const amountLeft = Amounts.sub(
-    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
-      .amount,
-    denom.fees.feeRefund,
-  ).amount;
-
-  const totalRefreshCostBound = getTotalRefreshCost(
-    allDenoms,
-    DenominationRecord.toDenomInfo(denom),
-    amountLeft,
-  );
-
-  p.refunds[refundKey] = {
-    type: RefundState.Pending,
-    obtainedTime: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
-    executionTime: r.execution_time,
-    refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.fees.feeRefund,
-    totalRefreshCostBound,
-    coinPub: r.coin_pub,
-    rtransactionId: r.rtransaction_id,
-  };
-}
-
-async function storeFailedRefund(
-  tx: GetReadWriteAccess<{
-    coins: typeof WalletStoresV1.coins;
-    denominations: typeof WalletStoresV1.denominations;
-  }>,
-  p: PurchaseRecord,
-  refreshCoinsMap: Record<string, { coinPub: string }>,
-  r: MerchantCoinRefundFailureStatus,
-): Promise<void> {
-  const refundKey = getRefundKey(r);
-
-  const coin = await tx.coins.get(r.coin_pub);
-  if (!coin) {
-    logger.warn("coin not found, can't apply refund");
-    return;
-  }
-  const denom = await tx.denominations.get([
-    coin.exchangeBaseUrl,
-    coin.denomPubHash,
-  ]);
-
-  if (!denom) {
-    throw Error("inconsistent database");
-  }
-
-  const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
-    .iter(coin.exchangeBaseUrl)
-    .toArray();
-
-  const amountLeft = Amounts.sub(
-    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
-      .amount,
-    denom.fees.feeRefund,
-  ).amount;
-
-  const totalRefreshCostBound = getTotalRefreshCost(
-    allDenoms,
-    DenominationRecord.toDenomInfo(denom),
-    amountLeft,
-  );
-
-  p.refunds[refundKey] = {
-    type: RefundState.Failed,
-    obtainedTime: TalerProtocolTimestamp.now(),
-    executionTime: r.execution_time,
-    refundAmount: Amounts.parseOrThrow(r.refund_amount),
-    refundFee: denom.fees.feeRefund,
-    totalRefreshCostBound,
-    coinPub: r.coin_pub,
-    rtransactionId: r.rtransaction_id,
-  };
-
-  if (p.abortStatus === AbortStatus.AbortRefund) {
-    // Refund failed because the merchant didn't even try to deposit
-    // the coin yet, so we try to refresh.
-    if (r.exchange_code === TalerErrorCode.EXCHANGE_REFUND_DEPOSIT_NOT_FOUND) {
-      const coin = await tx.coins.get(r.coin_pub);
-      if (!coin) {
-        logger.warn("coin not found, can't apply refund");
-        return;
-      }
-      const denom = await tx.denominations.get([
-        coin.exchangeBaseUrl,
-        coin.denomPubHash,
-      ]);
-      if (!denom) {
-        logger.warn("denomination for coin missing");
-        return;
-      }
-      let contrib: AmountJson | undefined;
-      for (let i = 0; i < p.payCoinSelection.coinPubs.length; i++) {
-        if (p.payCoinSelection.coinPubs[i] === r.coin_pub) {
-          contrib = p.payCoinSelection.coinContributions[i];
-        }
-      }
-      if (contrib) {
-        coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount;
-        coin.currentAmount = Amounts.sub(
-          coin.currentAmount,
-          denom.fees.feeRefund,
-        ).amount;
-      }
-      refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
-      await tx.coins.put(coin);
-    }
-  }
-}
-
-async function acceptRefunds(
-  ws: InternalWalletState,
-  proposalId: string,
-  refunds: MerchantCoinRefundStatus[],
-  reason: RefundReason,
-): Promise<void> {
-  logger.trace("handling refunds", refunds);
-  const now = TalerProtocolTimestamp.now();
-
-  await ws.db
-    .mktx((x) => [
-      x.purchases,
-      x.coins,
-      x.coinAvailability,
-      x.denominations,
-      x.refreshGroups,
-    ])
-    .runReadWrite(async (tx) => {
-      const p = await tx.purchases.get(proposalId);
-      if (!p) {
-        logger.error("purchase not found, not adding refunds");
-        return;
-      }
-
-      const refreshCoinsMap: Record<string, CoinPublicKey> = {};
-
-      for (const refundStatus of refunds) {
-        const refundKey = getRefundKey(refundStatus);
-        const existingRefundInfo = p.refunds[refundKey];
-
-        const isPermanentFailure =
-          refundStatus.type === "failure" &&
-          refundStatus.exchange_status >= 400 &&
-          refundStatus.exchange_status < 500;
-
-        // Already failed.
-        if (existingRefundInfo?.type === RefundState.Failed) {
-          continue;
-        }
-
-        // Already applied.
-        if (existingRefundInfo?.type === RefundState.Applied) {
-          continue;
-        }
-
-        // Still pending.
-        if (
-          refundStatus.type === "failure" &&
-          !isPermanentFailure &&
-          existingRefundInfo?.type === RefundState.Pending
-        ) {
-          continue;
-        }
-
-        // Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
-
-        if (refundStatus.type === "success") {
-          await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
-        } else if (isPermanentFailure) {
-          await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
-        } else {
-          await storePendingRefund(tx, p, refundStatus);
-        }
-      }
-
-      const refreshCoinsPubs = Object.values(refreshCoinsMap);
-      if (refreshCoinsPubs.length > 0) {
-        await createRefreshGroup(
-          ws,
-          tx,
-          refreshCoinsPubs,
-          RefreshReason.Refund,
-        );
-      }
-
-      // Are we done with querying yet, or do we need to do another round
-      // after a retry delay?
-      let queryDone = true;
-
-      if (
-        p.timestampFirstSuccessfulPay &&
-        p.autoRefundDeadline &&
-        AbsoluteTime.cmp(
-          AbsoluteTime.fromTimestamp(p.autoRefundDeadline),
-          AbsoluteTime.fromTimestamp(now),
-        ) > 0
-      ) {
-        queryDone = false;
-      }
-
-      let numPendingRefunds = 0;
-      for (const ri of Object.values(p.refunds)) {
-        switch (ri.type) {
-          case RefundState.Pending:
-            numPendingRefunds++;
-            break;
-        }
-      }
-
-      if (numPendingRefunds > 0) {
-        queryDone = false;
-      }
-
-      if (queryDone) {
-        p.timestampLastRefundStatus = now;
-        p.refundQueryRequested = false;
-        if (p.abortStatus === AbortStatus.AbortRefund) {
-          p.abortStatus = AbortStatus.AbortFinished;
-        }
-        logger.trace("refund query done");
-      } else {
-        // No error, but we need to try again!
-        p.timestampLastRefundStatus = now;
-        logger.trace("refund query not done");
-      }
-
-      await tx.purchases.put(p);
-    });
-
-  ws.notify({
-    type: NotificationType.RefundQueried,
-  });
-}
-
-function calculateRefundSummary(p: PurchaseRecord): RefundSummary {
-  let amountRefundGranted = Amounts.getZero(
-    p.download.contractData.amount.currency,
-  );
-  let amountRefundGone = Amounts.getZero(
-    p.download.contractData.amount.currency,
-  );
-
-  let pendingAtExchange = false;
-
-  Object.keys(p.refunds).forEach((rk) => {
-    const refund = p.refunds[rk];
-    if (refund.type === RefundState.Pending) {
-      pendingAtExchange = true;
-    }
-    if (
-      refund.type === RefundState.Applied ||
-      refund.type === RefundState.Pending
-    ) {
-      amountRefundGranted = Amounts.add(
-        amountRefundGranted,
-        Amounts.sub(
-          refund.refundAmount,
-          refund.refundFee,
-          refund.totalRefreshCostBound,
-        ).amount,
-      ).amount;
-    } else {
-      amountRefundGone = Amounts.add(
-        amountRefundGone,
-        refund.refundAmount,
-      ).amount;
-    }
-  });
-  return {
-    amountEffectivePaid: p.totalPayCost,
-    amountRefundGone,
-    amountRefundGranted,
-    pendingAtExchange,
-  };
-}
-
-/**
- * Summary of the refund status of a purchase.
- */
-export interface RefundSummary {
-  pendingAtExchange: boolean;
-  amountEffectivePaid: AmountJson;
-  amountRefundGranted: AmountJson;
-  amountRefundGone: AmountJson;
-}
-
-/**
- * Accept a refund, return the contract hash for the contract
- * that was involved in the refund.
- */
-export async function applyRefund(
-  ws: InternalWalletState,
-  talerRefundUri: string,
-): Promise<ApplyRefundResponse> {
-  const parseResult = parseRefundUri(talerRefundUri);
-
-  logger.trace("applying refund", parseResult);
-
-  if (!parseResult) {
-    throw Error("invalid refund URI");
-  }
-
-  const purchase = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadOnly(async (tx) => {
-      return tx.purchases.indexes.byMerchantUrlAndOrderId.get([
-        parseResult.merchantBaseUrl,
-        parseResult.orderId,
-      ]);
-    });
-
-  if (!purchase) {
-    throw Error(
-      `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
-    );
-  }
-
-  return applyRefundFromPurchaseId(ws, purchase.proposalId);
-}
-
-export async function applyRefundFromPurchaseId(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<ApplyRefundResponse> {
-  logger.trace("applying refund for purchase", proposalId);
-
-  logger.info("processing purchase for refund");
-  const success = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadWrite(async (tx) => {
-      const p = await tx.purchases.get(proposalId);
-      if (!p) {
-        logger.error("no purchase found for refund URL");
-        return false;
-      }
-      p.refundQueryRequested = true;
-      await tx.purchases.put(p);
-      return true;
-    });
-
-  if (success) {
-    ws.notify({
-      type: NotificationType.RefundStarted,
-    });
-    await processPurchaseQueryRefund(ws, proposalId, {
-      forceNow: true,
-      waitForAutoRefund: false,
-    });
-  }
-
-  const purchase = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadOnly(async (tx) => {
-      return tx.purchases.get(proposalId);
-    });
-
-  if (!purchase) {
-    throw Error("purchase no longer exists");
-  }
-
-  const summary = calculateRefundSummary(purchase);
-
-  return {
-    contractTermsHash: purchase.download.contractData.contractTermsHash,
-    proposalId: purchase.proposalId,
-    transactionId: makeEventId(TransactionType.Payment, proposalId), //FIXME: 
can we have the tx id of the refund
-    amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
-    amountRefundGone: Amounts.stringify(summary.amountRefundGone),
-    amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
-    pendingAtExchange: summary.pendingAtExchange,
-    info: {
-      contractTermsHash: purchase.download.contractData.contractTermsHash,
-      merchant: purchase.download.contractData.merchant,
-      orderId: purchase.download.contractData.orderId,
-      products: purchase.download.contractData.products,
-      summary: purchase.download.contractData.summary,
-      fulfillmentMessage: purchase.download.contractData.fulfillmentMessage,
-      summary_i18n: purchase.download.contractData.summaryI18n,
-      fulfillmentMessage_i18n:
-        purchase.download.contractData.fulfillmentMessageI18n,
-    },
-  };
-}
-
-async function queryAndSaveAwaitingRefund(
-  ws: InternalWalletState,
-  purchase: PurchaseRecord,
-  waitForAutoRefund?: boolean,
-): Promise<AmountJson> {
-  const requestUrl = new URL(
-    `orders/${purchase.download.contractData.orderId}`,
-    purchase.download.contractData.merchantBaseUrl,
-  );
-  requestUrl.searchParams.set(
-    "h_contract",
-    purchase.download.contractData.contractTermsHash,
-  );
-  // Long-poll for one second
-  if (waitForAutoRefund) {
-    requestUrl.searchParams.set("timeout_ms", "1000");
-    requestUrl.searchParams.set("await_refund_obtained", "yes");
-    logger.trace("making long-polling request for auto-refund");
-  }
-  const resp = await ws.http.get(requestUrl.href);
-  const orderStatus = await readSuccessResponseJsonOrThrow(
-    resp,
-    codecForMerchantOrderStatusPaid(),
-  );
-  if (!orderStatus.refunded) {
-    // Wait for retry ...
-    return Amounts.getZero(purchase.totalPayCost.currency);
-  }
-
-  const refundAwaiting = Amounts.sub(
-    Amounts.parseOrThrow(orderStatus.refund_amount),
-    Amounts.parseOrThrow(orderStatus.refund_taken),
-  ).amount;
-
-  if (
-    purchase.refundAwaiting === undefined ||
-    Amounts.cmp(refundAwaiting, purchase.refundAwaiting) !== 0
-  ) {
-    await ws.db
-      .mktx((x) => [x.purchases])
-      .runReadWrite(async (tx) => {
-        const p = await tx.purchases.get(purchase.proposalId);
-        if (!p) {
-          logger.warn("purchase does not exist anymore");
-          return;
-        }
-        p.refundAwaiting = refundAwaiting;
-        await tx.purchases.put(p);
-      });
-  }
-
-  return refundAwaiting;
-}
-
-export async function processPurchaseQueryRefund(
-  ws: InternalWalletState,
-  proposalId: string,
-  options: {
-    forceNow?: boolean;
-    waitForAutoRefund?: boolean;
-  } = {},
-): Promise<OperationAttemptResult> {
-  const waitForAutoRefund = options.waitForAutoRefund ?? false;
-  const purchase = await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadOnly(async (tx) => {
-      return tx.purchases.get(proposalId);
-    });
-  if (!purchase) {
-    return OperationAttemptResult.finishedEmpty();
-  }
-
-  if (!purchase.refundQueryRequested) {
-    return OperationAttemptResult.finishedEmpty();
-  }
-
-  if (purchase.timestampFirstSuccessfulPay) {
-    if (
-      !purchase.autoRefundDeadline ||
-      !AbsoluteTime.isExpired(
-        AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline),
-      )
-    ) {
-      const awaitingAmount = await queryAndSaveAwaitingRefund(
-        ws,
-        purchase,
-        waitForAutoRefund,
-      );
-      if (Amounts.isZero(awaitingAmount)) {
-        return OperationAttemptResult.finishedEmpty();
-      }
-    }
-
-    const requestUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/refund`,
-      purchase.download.contractData.merchantBaseUrl,
-    );
-
-    logger.trace(`making refund request to ${requestUrl.href}`);
-
-    const request = await ws.http.postJson(requestUrl.href, {
-      h_contract: purchase.download.contractData.contractTermsHash,
-    });
-
-    const refundResponse = await readSuccessResponseJsonOrThrow(
-      request,
-      codecForMerchantOrderRefundPickupResponse(),
-    );
-
-    await acceptRefunds(
-      ws,
-      proposalId,
-      refundResponse.refunds,
-      RefundReason.NormalRefund,
-    );
-  } else if (purchase.abortStatus === AbortStatus.AbortRefund) {
-    const requestUrl = new URL(
-      `orders/${purchase.download.contractData.orderId}/abort`,
-      purchase.download.contractData.merchantBaseUrl,
-    );
-
-    const abortingCoins: AbortingCoin[] = [];
-
-    await ws.db
-      .mktx((x) => [x.coins])
-      .runReadOnly(async (tx) => {
-        for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
-          const coinPub = purchase.payCoinSelection.coinPubs[i];
-          const coin = await tx.coins.get(coinPub);
-          checkDbInvariant(!!coin, "expected coin to be present");
-          abortingCoins.push({
-            coin_pub: coinPub,
-            contribution: Amounts.stringify(
-              purchase.payCoinSelection.coinContributions[i],
-            ),
-            exchange_url: coin.exchangeBaseUrl,
-          });
-        }
-      });
-
-    const abortReq: AbortRequest = {
-      h_contract: purchase.download.contractData.contractTermsHash,
-      coins: abortingCoins,
-    };
-
-    logger.trace(`making order abort request to ${requestUrl.href}`);
-
-    const request = await ws.http.postJson(requestUrl.href, abortReq);
-    const abortResp = await readSuccessResponseJsonOrThrow(
-      request,
-      codecForAbortResponse(),
-    );
-
-    const refunds: MerchantCoinRefundStatus[] = [];
-
-    if (abortResp.refunds.length != abortingCoins.length) {
-      // FIXME: define error code!
-      throw Error("invalid order abort response");
-    }
-
-    for (let i = 0; i < abortResp.refunds.length; i++) {
-      const r = abortResp.refunds[i];
-      refunds.push({
-        ...r,
-        coin_pub: purchase.payCoinSelection.coinPubs[i],
-        refund_amount: Amounts.stringify(
-          purchase.payCoinSelection.coinContributions[i],
-        ),
-        rtransaction_id: 0,
-        execution_time: AbsoluteTime.toTimestamp(
-          AbsoluteTime.addDuration(
-            AbsoluteTime.fromTimestamp(
-              purchase.download.contractData.timestamp,
-            ),
-            Duration.fromSpec({ seconds: 1 }),
-          ),
-        ),
-      });
-    }
-    await acceptRefunds(ws, proposalId, refunds, RefundReason.AbortRefund);
-  }
-  return OperationAttemptResult.finishedEmpty();
-}
-
-export async function abortFailedPayWithRefund(
-  ws: InternalWalletState,
-  proposalId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.purchases])
-    .runReadWrite(async (tx) => {
-      const purchase = await tx.purchases.get(proposalId);
-      if (!purchase) {
-        throw Error("purchase not found");
-      }
-      if (purchase.timestampFirstSuccessfulPay) {
-        // No point in aborting it.  We don't even report an error.
-        logger.warn(`tried to abort successful payment`);
-        return;
-      }
-      if (purchase.abortStatus !== AbortStatus.None) {
-        return;
-      }
-      purchase.refundQueryRequested = true;
-      purchase.paymentSubmitPending = false;
-      purchase.abortStatus = AbortStatus.AbortRefund;
-      await tx.purchases.put(purchase);
-    });
-  processPurchaseQueryRefund(ws, proposalId, {
-    forceNow: true,
-  }).catch((e) => {
-    logger.trace(`error during refund processing after abort pay: ${e}`);
-  });
-}
diff --git a/packages/taler-wallet-core/src/operations/testing.ts 
b/packages/taler-wallet-core/src/operations/testing.ts
index 598a88502..9a11af8bb 100644
--- a/packages/taler-wallet-core/src/operations/testing.ts
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -40,9 +40,8 @@ import {
   PreparePayResultType,
 } from "@gnu-taler/taler-util";
 import { InternalWalletState } from "../internal-wallet-state.js";
-import { confirmPay, preparePayForUri } from "./pay.js";
+import { applyRefund, confirmPay, preparePayForUri } from "./pay-merchant.js";
 import { getBalances } from "./balance.js";
-import { applyRefund } from "./refund.js";
 import { checkLogicInvariant } from "../util/invariants.js";
 import { acceptWithdrawalFromUri } from "./withdraw.js";
 
@@ -471,6 +470,6 @@ export async function testPay(
     });
   checkLogicInvariant(!!purchase);
   return {
-    payCoinSelection: purchase.payCoinSelection,
+    payCoinSelection: purchase.payInfo?.payCoinSelection!,
   };
 }
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index bd5ff51e7..a83867f55 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -18,8 +18,8 @@
  * Imports.
  */
 import {
-  AgeRestriction,
   AcceptTipResponse,
+  AgeRestriction,
   Amounts,
   BlindedDenominationSignature,
   codecForMerchantTipResponseV2,
@@ -56,9 +56,8 @@ import {
   OperationAttemptResult,
   OperationAttemptResultType,
 } from "../util/retries.js";
-import { makeCoinAvailable } from "../wallet.js";
+import { makeCoinAvailable, makeEventId } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
-import { makeEventId } from "./transactions.js";
 import {
   getCandidateWithdrawalDenoms,
   getExchangeWithdrawalInfo,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 4086fc9b3..d8069436a 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -36,12 +36,12 @@ import {
   WithdrawalType,
 } from "@gnu-taler/taler-util";
 import {
-  AbortStatus,
   DepositGroupRecord,
   ExchangeDetailsRecord,
   OperationRetryRecord,
   PeerPullPaymentIncomingRecord,
   PeerPushPaymentInitiationRecord,
+  PurchaseStatus,
   PurchaseRecord,
   RefundState,
   TipRecord,
@@ -50,10 +50,12 @@ import {
   WithdrawalRecordType,
 } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
+import { checkDbInvariant } from "../util/invariants.js";
 import { RetryTags } from "../util/retries.js";
+import { makeEventId, TombstoneTag } from "./common.js";
 import { processDepositGroup } from "./deposits.js";
 import { getExchangeDetails } from "./exchanges.js";
-import { processPurchasePay } from "./pay.js";
+import { expectProposalDownload, processPurchasePay } from "./pay-merchant.js";
 import { processRefreshGroup } from "./refresh.js";
 import { processTip } from "./tip.js";
 import {
@@ -63,28 +65,6 @@ import {
 
 const logger = new Logger("taler-wallet-core:transactions.ts");
 
-export enum TombstoneTag {
-  DeleteWithdrawalGroup = "delete-withdrawal-group",
-  DeleteReserve = "delete-reserve",
-  DeletePayment = "delete-payment",
-  DeleteTip = "delete-tip",
-  DeleteRefreshGroup = "delete-refresh-group",
-  DeleteDepositGroup = "delete-deposit-group",
-  DeleteRefund = "delete-refund",
-  DeletePeerPullDebit = "delete-peer-pull-debit",
-  DeletePeerPushDebit = "delete-peer-push-debit",
-}
-
-/**
- * Create an event ID from the type and the primary key for the event.
- */
-export function makeEventId(
-  type: TransactionType | TombstoneTag,
-  ...args: string[]
-): string {
-  return type + ":" + args.map((x) => encodeURIComponent(x)).join(":");
-}
-
 function shouldSkipCurrency(
   transactionsRequest: TransactionsRequest | undefined,
   currency: string,
@@ -219,29 +199,22 @@ export async function getTransactionById(
           }),
         );
 
+        const download = await expectProposalDownload(purchase);
+
         const cleanRefunds = filteredRefunds.filter(
           (x): x is WalletRefundItem => !!x,
         );
 
-        const contractData = purchase.download.contractData;
+        const contractData = download.contractData;
         const refunds = mergeRefundByExecutionTime(
           cleanRefunds,
           Amounts.getZero(contractData.amount.currency),
         );
 
         const payOpId = RetryTags.forPay(purchase);
-        const refundQueryOpId = RetryTags.forRefundQuery(purchase);
         const payRetryRecord = await tx.operationRetries.get(payOpId);
-        const refundQueryRetryRecord = await tx.operationRetries.get(
-          refundQueryOpId,
-        );
-
-        const err =
-          payRetryRecord !== undefined
-            ? payRetryRecord
-            : refundQueryRetryRecord;
 
-        return buildTransactionForPurchase(purchase, refunds, err);
+        return buildTransactionForPurchase(purchase, refunds, payRetryRecord);
       });
   } else if (type === TransactionType.Refresh) {
     const refreshGroupId = rest[0];
@@ -295,23 +268,14 @@ export async function getTransactionById(
           ),
         );
         if (t) throw Error("deleted");
-
-        const contractData = purchase.download.contractData;
+        const download = await expectProposalDownload(purchase);
+        const contractData = download.contractData;
         const refunds = mergeRefundByExecutionTime(
           [theRefund],
           Amounts.getZero(contractData.amount.currency),
         );
 
-        const refundQueryOpId = RetryTags.forRefundQuery(purchase);
-        const refundQueryRetryRecord = await tx.operationRetries.get(
-          refundQueryOpId,
-        );
-
-        return buildTransactionForRefund(
-          purchase,
-          refunds[0],
-          refundQueryRetryRecord,
-        );
+        return buildTransactionForRefund(purchase, refunds[0], undefined);
       });
   } else if (type === TransactionType.PeerPullDebit) {
     const peerPullPaymentIncomingId = rest[0];
@@ -606,12 +570,13 @@ function mergeRefundByExecutionTime(
   return Array.from(refundByExecTime.values());
 }
 
-function buildTransactionForRefund(
+async function buildTransactionForRefund(
   purchaseRecord: PurchaseRecord,
   refundInfo: MergedRefundInfo,
   ort?: OperationRetryRecord,
-): Transaction {
-  const contractData = purchaseRecord.download.contractData;
+): Promise<Transaction> {
+  const download = await expectProposalDownload(purchaseRecord);
+  const contractData = download.contractData;
 
   const info: OrderShortInfo = {
     merchant: contractData.merchant,
@@ -641,21 +606,22 @@ function buildTransactionForRefund(
     amountEffective: Amounts.stringify(refundInfo.amountAppliedEffective),
     amountRaw: Amounts.stringify(refundInfo.amountAppliedRaw),
     refundPending:
-      purchaseRecord.refundAwaiting === undefined
+      purchaseRecord.refundAmountAwaiting === undefined
         ? undefined
-        : Amounts.stringify(purchaseRecord.refundAwaiting),
+        : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
     pending: false,
     frozen: false,
     ...(ort?.lastError ? { error: ort.lastError } : {}),
   };
 }
 
-function buildTransactionForPurchase(
+async function buildTransactionForPurchase(
   purchaseRecord: PurchaseRecord,
   refundsInfo: MergedRefundInfo[],
   ort?: OperationRetryRecord,
-): Transaction {
-  const contractData = purchaseRecord.download.contractData;
+): Promise<Transaction> {
+  const download = await expectProposalDownload(purchaseRecord);
+  const contractData = download.contractData;
   const zero = Amounts.getZero(contractData.amount.currency);
 
   const info: OrderShortInfo = {
@@ -696,31 +662,34 @@ function buildTransactionForPurchase(
     ),
   }));
 
+  const timestamp = purchaseRecord.timestampAccept;
+  checkDbInvariant(!!timestamp);
+  checkDbInvariant(!!purchaseRecord.payInfo);
+
   return {
     type: TransactionType.Payment,
     amountRaw: Amounts.stringify(contractData.amount),
-    amountEffective: Amounts.stringify(purchaseRecord.totalPayCost),
+    amountEffective: Amounts.stringify(purchaseRecord.payInfo.totalPayCost),
     totalRefundRaw: Amounts.stringify(totalRefund.raw),
     totalRefundEffective: Amounts.stringify(totalRefund.effective),
     refundPending:
-      purchaseRecord.refundAwaiting === undefined
+      purchaseRecord.refundAmountAwaiting === undefined
         ? undefined
-        : Amounts.stringify(purchaseRecord.refundAwaiting),
+        : Amounts.stringify(purchaseRecord.refundAmountAwaiting),
     status: purchaseRecord.timestampFirstSuccessfulPay
       ? PaymentStatus.Paid
       : PaymentStatus.Accepted,
-    pending:
-      !purchaseRecord.timestampFirstSuccessfulPay &&
-      purchaseRecord.abortStatus === AbortStatus.None,
+    pending: purchaseRecord.purchaseStatus === PurchaseStatus.Paying,
     refunds,
-    timestamp: purchaseRecord.timestampAccept,
+    timestamp,
     transactionId: makeEventId(
       TransactionType.Payment,
       purchaseRecord.proposalId,
     ),
     proposalId: purchaseRecord.proposalId,
     info,
-    frozen: purchaseRecord.payFrozen ?? false,
+    frozen:
+      purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ?? 
false,
     ...(ort?.lastError ? { error: ort.lastError } : {}),
   };
 }
@@ -745,7 +714,6 @@ export async function getTransactions(
       x.peerPullPaymentIncoming,
       x.peerPushPaymentInitiations,
       x.planchets,
-      x.proposals,
       x.purchases,
       x.recoupGroups,
       x.tips,
@@ -838,30 +806,33 @@ export async function getTransactions(
         transactions.push(buildTransactionForDeposit(dg, retryRecord));
       });
 
-      tx.purchases.iter().forEachAsync(async (pr) => {
+      tx.purchases.iter().forEachAsync(async (purchase) => {
+        const download = purchase.download;
+        if (!download) {
+          return;
+        }
+        if (!purchase.payInfo) {
+          return;
+        }
         if (
           shouldSkipCurrency(
             transactionsRequest,
-            pr.download.contractData.amount.currency,
+            download.contractData.amount.currency,
           )
         ) {
           return;
         }
-        const contractData = pr.download.contractData;
+        const contractData = download.contractData;
         if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
           return;
         }
-        const proposal = await tx.proposals.get(pr.proposalId);
-        if (!proposal) {
-          return;
-        }
 
         const filteredRefunds = await Promise.all(
-          Object.values(pr.refunds).map(async (r) => {
+          Object.values(purchase.refunds).map(async (r) => {
             const t = await tx.tombstones.get(
               makeEventId(
                 TombstoneTag.DeleteRefund,
-                pr.proposalId,
+                purchase.proposalId,
                 `${r.executionTime.t_s}`,
               ),
             );
@@ -880,29 +851,16 @@ export async function getTransactions(
         );
 
         refunds.forEach(async (refundInfo) => {
-          const refundQueryOpId = RetryTags.forRefundQuery(pr);
-          const refundQueryRetryRecord = await tx.operationRetries.get(
-            refundQueryOpId,
-          );
-
           transactions.push(
-            buildTransactionForRefund(pr, refundInfo, refundQueryRetryRecord),
+            await buildTransactionForRefund(purchase, refundInfo, undefined),
           );
         });
 
-        const payOpId = RetryTags.forPay(pr);
-        const refundQueryOpId = RetryTags.forRefundQuery(pr);
+        const payOpId = RetryTags.forPay(purchase);
         const payRetryRecord = await tx.operationRetries.get(payOpId);
-        const refundQueryRetryRecord = await tx.operationRetries.get(
-          refundQueryOpId,
+        transactions.push(
+          await buildTransactionForPurchase(purchase, refunds, payRetryRecord),
         );
-
-        const err =
-          payRetryRecord !== undefined
-            ? payRetryRecord
-            : refundQueryRetryRecord;
-
-        transactions.push(buildTransactionForPurchase(pr, refunds, err));
       });
 
       tx.tips.iter().forEachAsync(async (tipRecord) => {
@@ -1020,14 +978,9 @@ export async function deleteTransaction(
   } else if (type === TransactionType.Payment) {
     const proposalId = rest[0];
     await ws.db
-      .mktx((x) => [x.proposals, x.purchases, x.tombstones])
+      .mktx((x) => [x.purchases, x.tombstones])
       .runReadWrite(async (tx) => {
         let found = false;
-        const proposal = await tx.proposals.get(proposalId);
-        if (proposal) {
-          found = true;
-          await tx.proposals.delete(proposalId);
-        }
         const purchase = await tx.purchases.get(proposalId);
         if (purchase) {
           found = true;
@@ -1083,7 +1036,7 @@ export async function deleteTransaction(
     const executionTimeStr = rest[1];
 
     await ws.db
-      .mktx((x) => [x.proposals, x.purchases, x.tombstones])
+      .mktx((x) => [x.purchases, x.tombstones])
       .runReadWrite(async (tx) => {
         const purchase = await tx.purchases.get(proposalId);
         if (purchase) {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index fb5e2c70a..3c2541e9a 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -70,12 +70,11 @@ import {
   DenomSelectionState,
   ExchangeDetailsRecord,
   ExchangeRecord,
-  OperationStatus,
   PlanchetRecord,
-  WithdrawalGroupStatus,
   WalletStoresV1,
   WgInfo,
   WithdrawalGroupRecord,
+  WithdrawalGroupStatus,
   WithdrawalRecordType,
 } from "../db.js";
 import {
@@ -84,7 +83,10 @@ import {
   TalerError,
 } from "../errors.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
+import {
+  makeCoinAvailable,
+  runOperationWithErrorReporting,
+} from "../operations/common.js";
 import { walletCoreDebugFlags } from "../util/debugFlags.js";
 import {
   HttpRequestLibrary,
@@ -108,18 +110,16 @@ import {
   WALLET_EXCHANGE_PROTOCOL_VERSION,
 } from "../versions.js";
 import {
-  makeCoinAvailable,
-  runOperationWithErrorReporting,
+  makeEventId,
   storeOperationError,
   storeOperationPending,
-} from "../wallet.js";
+} from "./common.js";
 import {
   getExchangeDetails,
   getExchangePaytoUri,
   getExchangeTrust,
   updateExchangeFromUrl,
 } from "./exchanges.js";
-import { makeEventId } from "./transactions.js";
 
 /**
  * Logger for this file.
diff --git a/packages/taler-wallet-core/src/pending-types.ts 
b/packages/taler-wallet-core/src/pending-types.ts
index 5e0000b53..862bbf4f9 100644
--- a/packages/taler-wallet-core/src/pending-types.ts
+++ b/packages/taler-wallet-core/src/pending-types.ts
@@ -34,11 +34,9 @@ import { RetryInfo } from "./util/retries.js";
 export enum PendingTaskType {
   ExchangeUpdate = "exchange-update",
   ExchangeCheckRefresh = "exchange-check-refresh",
-  Pay = "pay",
-  ProposalDownload = "proposal-download",
+  Purchase = "purchase",
   Refresh = "refresh",
   Recoup = "recoup",
-  RefundQuery = "refund-query",
   TipPickup = "tip-pickup",
   Withdraw = "withdraw",
   Deposit = "deposit",
@@ -52,10 +50,8 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
   (
     | PendingExchangeUpdateTask
     | PendingExchangeCheckRefreshTask
-    | PendingPayTask
-    | PendingProposalDownloadTask
+    | PendingPurchaseTask
     | PendingRefreshTask
-    | PendingRefundQueryTask
     | PendingTipPickupTask
     | PendingWithdrawTask
     | PendingRecoupTask
@@ -109,19 +105,6 @@ export interface PendingRefreshTask {
   retryInfo?: RetryInfo;
 }
 
-/**
- * Status of downloading signed contract terms from a merchant.
- */
-export interface PendingProposalDownloadTask {
-  type: PendingTaskType.ProposalDownload;
-  merchantBaseUrl: string;
-  proposalTimestamp: TalerProtocolTimestamp;
-  proposalId: string;
-  orderId: string;
-  lastError?: TalerErrorDetail;
-  retryInfo?: RetryInfo;
-}
-
 /**
  * The wallet is picking up a tip that the user has accepted.
  */
@@ -133,25 +116,16 @@ export interface PendingTipPickupTask {
 }
 
 /**
- * The wallet is signing coins and then sending them to
- * the merchant.
+ * A purchase needs to be processed (i.e. for download / payment / refund).
  */
-export interface PendingPayTask {
-  type: PendingTaskType.Pay;
-  proposalId: string;
-  isReplay: boolean;
-  retryInfo?: RetryInfo;
-  lastError: TalerErrorDetail | undefined;
-}
-
-/**
- * The wallet is querying the merchant about whether any refund
- * permissions are available for a purchase.
- */
-export interface PendingRefundQueryTask {
-  type: PendingTaskType.RefundQuery;
+export interface PendingPurchaseTask {
+  type: PendingTaskType.Purchase;
   proposalId: string;
   retryInfo?: RetryInfo;
+  /**
+   * Status of the payment as string, used only for debugging.
+   */
+  statusStr: string;
   lastError: TalerErrorDetail | undefined;
 }
 
diff --git a/packages/taler-wallet-core/src/util/retries.ts 
b/packages/taler-wallet-core/src/util/retries.ts
index cef9e072c..697d6531e 100644
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ b/packages/taler-wallet-core/src/util/retries.ts
@@ -30,7 +30,6 @@ import {
   BackupProviderRecord,
   DepositGroupRecord,
   ExchangeRecord,
-  ProposalRecord,
   PurchaseRecord,
   RecoupGroupRecord,
   RefreshGroupRecord,
@@ -181,9 +180,6 @@ export namespace RetryTags {
   export function forExchangeCheckRefresh(exch: ExchangeRecord): string {
     return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`;
   }
-  export function forProposalClaim(pr: ProposalRecord): string {
-    return `${PendingTaskType.ProposalDownload}:${pr.proposalId}`;
-  }
   export function forTipPickup(tipRecord: TipRecord): string {
     return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}`;
   }
@@ -191,10 +187,7 @@ export namespace RetryTags {
     return `${PendingTaskType.TipPickup}:${refreshGroupRecord.refreshGroupId}`;
   }
   export function forPay(purchaseRecord: PurchaseRecord): string {
-    return `${PendingTaskType.Pay}:${purchaseRecord.proposalId}`;
-  }
-  export function forRefundQuery(purchaseRecord: PurchaseRecord): string {
-    return `${PendingTaskType.RefundQuery}:${purchaseRecord.proposalId}`;
+    return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}`;
   }
   export function forRecoup(recoupRecord: RecoupGroupRecord): string {
     return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}`;
@@ -206,7 +199,7 @@ export namespace RetryTags {
     return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
   }
   export function byPaymentProposalId(proposalId: string): string {
-    return `${PendingTaskType.Pay}:${proposalId}`;
+    return `${PendingTaskType.Purchase}:${proposalId}`;
   }
 }
 
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index aa3810035..07dd1fcda 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -26,7 +26,6 @@ import {
   AbsoluteTime,
   AmountJson,
   Amounts,
-  BalancesResponse,
   codecForAbortPayWithRefundRequest,
   codecForAcceptBankIntegratedWithdrawalRequest,
   codecForAcceptExchangeTosRequest,
@@ -35,6 +34,7 @@ import {
   codecForAcceptPeerPushPaymentRequest,
   codecForAcceptTipRequest,
   codecForAddExchangeRequest,
+  codecForAddKnownBankAccounts,
   codecForAny,
   codecForApplyRefundFromPurchaseIdRequest,
   codecForApplyRefundRequest,
@@ -44,6 +44,7 @@ import {
   codecForCreateDepositGroupRequest,
   codecForDeleteTransactionRequest,
   codecForForceRefreshRequest,
+  codecForForgetKnownBankAccounts,
   codecForGetContractTermsDetails,
   codecForGetExchangeTosRequest,
   codecForGetExchangeWithdrawalInfo,
@@ -81,6 +82,7 @@ import {
   GetExchangeTosResult,
   j2s,
   KnownBankAccounts,
+  KnownBankAccountsInfo,
   Logger,
   ManualWithdrawalDetails,
   NotificationType,
@@ -89,9 +91,6 @@ import {
   RefreshReason,
   TalerErrorCode,
   TalerErrorDetail,
-  KnownBankAccountsInfo,
-  codecForAddKnownBankAccounts,
-  codecForForgetKnownBankAccounts,
   URL,
   WalletCoreVersion,
   WalletNotification,
@@ -125,6 +124,7 @@ import {
   MerchantOperations,
   NotificationListener,
   RecoupOperations,
+  RefreshOperations,
 } from "./internal-wallet-state.js";
 import { exportBackup } from "./operations/backup/export.js";
 import {
@@ -142,6 +142,11 @@ import {
 } from "./operations/backup/index.js";
 import { setWalletDeviceId } from "./operations/backup/state.js";
 import { getBalances } from "./operations/balance.js";
+import {
+  runOperationWithErrorReporting,
+  storeOperationError,
+  storeOperationPending,
+} from "./operations/common.js";
 import {
   createDepositGroup,
   getFeeForDeposit,
@@ -162,12 +167,15 @@ import {
 } from "./operations/exchanges.js";
 import { getMerchantInfo } from "./operations/merchants.js";
 import {
+  abortFailedPayWithRefund,
+  applyRefund,
+  applyRefundFromPurchaseId,
   confirmPay,
   getContractTermsDetails,
   preparePayForUri,
-  processDownloadProposal,
-  processPurchasePay,
-} from "./operations/pay.js";
+  prepareRefund,
+  processPurchase,
+} from "./operations/pay-merchant.js";
 import {
   acceptPeerPullPayment,
   acceptPeerPushPayment,
@@ -175,7 +183,7 @@ import {
   checkPeerPushPayment,
   initiatePeerRequestForPay,
   initiatePeerToPeerPush,
-} from "./operations/peer-to-peer.js";
+} from "./operations/pay-peer.js";
 import { getPendingOperations } from "./operations/pending.js";
 import {
   createRecoupGroup,
@@ -187,13 +195,6 @@ import {
   createRefreshGroup,
   processRefreshGroup,
 } from "./operations/refresh.js";
-import {
-  abortFailedPayWithRefund,
-  applyRefund,
-  applyRefundFromPurchaseId,
-  prepareRefund,
-  processPurchaseQueryRefund,
-} from "./operations/refund.js";
 import {
   runIntegrationTest,
   testPay,
@@ -213,13 +214,8 @@ import {
   getWithdrawalDetailsForUri,
   processWithdrawalGroup,
 } from "./operations/withdraw.js";
-import {
-  PendingOperationsResponse,
-  PendingTaskInfo,
-  PendingTaskType,
-} from "./pending-types.js";
+import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
 import { assertUnreachable } from "./util/assertUnreachable.js";
-import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js";
 import { createDenominationTimeline } from "./util/denominations.js";
 import {
   HttpRequestLibrary,
@@ -306,18 +302,10 @@ async function callOperationHandler(
       return await processWithdrawalGroup(ws, pending.withdrawalGroupId, {
         forceNow,
       });
-    case PendingTaskType.ProposalDownload:
-      return await processDownloadProposal(ws, pending.proposalId, {
-        forceNow,
-      });
     case PendingTaskType.TipPickup:
       return await processTip(ws, pending.tipId, { forceNow });
-    case PendingTaskType.Pay:
-      return await processPurchasePay(ws, pending.proposalId, { forceNow });
-    case PendingTaskType.RefundQuery:
-      return await processPurchaseQueryRefund(ws, pending.proposalId, {
-        forceNow,
-      });
+    case PendingTaskType.Purchase:
+      return await processPurchase(ws, pending.proposalId, { forceNow });
     case PendingTaskType.Recoup:
       return await processRecoupGroupHandler(ws, pending.recoupGroupId, {
         forceNow,
@@ -337,111 +325,6 @@ async function callOperationHandler(
   throw Error(`not reached ${pending.type}`);
 }
 
-export async function storeOperationError(
-  ws: InternalWalletState,
-  pendingTaskId: string,
-  e: TalerErrorDetail,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      let retryRecord = await tx.operationRetries.get(pendingTaskId);
-      if (!retryRecord) {
-        retryRecord = {
-          id: pendingTaskId,
-          lastError: e,
-          retryInfo: RetryInfo.reset(),
-        };
-      } else {
-        retryRecord.lastError = e;
-        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
-      }
-      await tx.operationRetries.put(retryRecord);
-    });
-}
-
-export async function storeOperationFinished(
-  ws: InternalWalletState,
-  pendingTaskId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      await tx.operationRetries.delete(pendingTaskId);
-    });
-}
-
-export async function storeOperationPending(
-  ws: InternalWalletState,
-  pendingTaskId: string,
-): Promise<void> {
-  await ws.db
-    .mktx((x) => [x.operationRetries])
-    .runReadWrite(async (tx) => {
-      let retryRecord = await tx.operationRetries.get(pendingTaskId);
-      if (!retryRecord) {
-        retryRecord = {
-          id: pendingTaskId,
-          retryInfo: RetryInfo.reset(),
-        };
-      } else {
-        delete retryRecord.lastError;
-        retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
-      }
-      await tx.operationRetries.put(retryRecord);
-    });
-}
-
-export async function runOperationWithErrorReporting(
-  ws: InternalWalletState,
-  opId: string,
-  f: () => Promise<OperationAttemptResult>,
-): Promise<void> {
-  let maybeError: TalerErrorDetail | undefined;
-  try {
-    const resp = await f();
-    switch (resp.type) {
-      case OperationAttemptResultType.Error:
-        return await storeOperationError(ws, opId, resp.errorDetail);
-      case OperationAttemptResultType.Finished:
-        return await storeOperationFinished(ws, opId);
-      case OperationAttemptResultType.Pending:
-        return await storeOperationPending(ws, opId);
-      case OperationAttemptResultType.Longpoll:
-        break;
-    }
-  } catch (e) {
-    if (e instanceof TalerError) {
-      logger.warn("operation processed resulted in error");
-      logger.warn(`error was: ${j2s(e.errorDetail)}`);
-      maybeError = e.errorDetail;
-      return await storeOperationError(ws, opId, maybeError!);
-    } else if (e instanceof Error) {
-      // This is a bug, as we expect pending operations to always
-      // do their own error handling and only throw 
WALLET_PENDING_OPERATION_FAILED
-      // or return something.
-      logger.error(`Uncaught exception: ${e.message}`);
-      logger.error(`Stack: ${e.stack}`);
-      maybeError = makeErrorDetail(
-        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-        {
-          stack: e.stack,
-        },
-        `unexpected exception (message: ${e.message})`,
-      );
-      return await storeOperationError(ws, opId, maybeError);
-    } else {
-      logger.error("Uncaught exception, value is not even an error.");
-      maybeError = makeErrorDetail(
-        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
-        {},
-        `unexpected exception (not even an error)`,
-      );
-      return await storeOperationError(ws, opId, maybeError);
-    }
-  }
-}
-
 /**
  * Process pending operations.
  */
@@ -857,120 +740,6 @@ async function getExchangeDetailedInfo(
   };
 }
 
-export async function makeCoinAvailable(
-  ws: InternalWalletState,
-  tx: GetReadWriteAccess<{
-    coins: typeof WalletStoresV1.coins;
-    coinAvailability: typeof WalletStoresV1.coinAvailability;
-    denominations: typeof WalletStoresV1.denominations;
-  }>,
-  coinRecord: CoinRecord,
-): Promise<void> {
-  checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
-  const existingCoin = await tx.coins.get(coinRecord.coinPub);
-  if (existingCoin) {
-    return;
-  }
-  const denom = await tx.denominations.get([
-    coinRecord.exchangeBaseUrl,
-    coinRecord.denomPubHash,
-  ]);
-  checkDbInvariant(!!denom);
-  const ageRestriction = coinRecord.maxAge;
-  let car = await tx.coinAvailability.get([
-    coinRecord.exchangeBaseUrl,
-    coinRecord.denomPubHash,
-    ageRestriction,
-  ]);
-  if (!car) {
-    car = {
-      maxAge: ageRestriction,
-      amountFrac: denom.amountFrac,
-      amountVal: denom.amountVal,
-      currency: denom.currency,
-      denomPubHash: denom.denomPubHash,
-      exchangeBaseUrl: denom.exchangeBaseUrl,
-      freshCoinCount: 0,
-    };
-  }
-  car.freshCoinCount++;
-  await tx.coins.put(coinRecord);
-  await tx.coinAvailability.put(car);
-}
-
-export interface CoinsSpendInfo {
-  coinPubs: string[];
-  contributions: AmountJson[];
-  refreshReason: RefreshReason;
-  /**
-   * Identifier for what the coin has been spent for.
-   */
-  allocationId: string;
-}
-
-export async function spendCoins(
-  ws: InternalWalletState,
-  tx: GetReadWriteAccess<{
-    coins: typeof WalletStoresV1.coins;
-    coinAvailability: typeof WalletStoresV1.coinAvailability;
-    refreshGroups: typeof WalletStoresV1.refreshGroups;
-    denominations: typeof WalletStoresV1.denominations;
-  }>,
-  csi: CoinsSpendInfo,
-): Promise<void> {
-  for (let i = 0; i < csi.coinPubs.length; i++) {
-    const coin = await tx.coins.get(csi.coinPubs[i]);
-    if (!coin) {
-      throw Error("coin allocated for payment doesn't exist anymore");
-    }
-    const coinAvailability = await tx.coinAvailability.get([
-      coin.exchangeBaseUrl,
-      coin.denomPubHash,
-      coin.maxAge,
-    ]);
-    checkDbInvariant(!!coinAvailability);
-    const contrib = csi.contributions[i];
-    if (coin.status !== CoinStatus.Fresh) {
-      const alloc = coin.allocation;
-      if (!alloc) {
-        continue;
-      }
-      if (alloc.id !== csi.allocationId) {
-        // FIXME: assign error code
-        throw Error("conflicting coin allocation (id)");
-      }
-      if (0 !== Amounts.cmp(alloc.amount, contrib)) {
-        // FIXME: assign error code
-        throw Error("conflicting coin allocation (contrib)");
-      }
-      continue;
-    }
-    coin.status = CoinStatus.Dormant;
-    coin.allocation = {
-      id: csi.allocationId,
-      amount: Amounts.stringify(contrib),
-    };
-    const remaining = Amounts.sub(coin.currentAmount, contrib);
-    if (remaining.saturated) {
-      throw Error("not enough remaining balance on coin for payment");
-    }
-    coin.currentAmount = remaining.amount;
-    checkDbInvariant(!!coinAvailability);
-    if (coinAvailability.freshCoinCount === 0) {
-      throw Error(
-        `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
-      );
-    }
-    coinAvailability.freshCoinCount--;
-    await tx.coins.put(coin);
-    await tx.coinAvailability.put(coinAvailability);
-  }
-  const refreshCoinPubs = csi.coinPubs.map((x) => ({
-    coinPub: x,
-  }));
-  await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant);
-}
-
 async function setCoinSuspended(
   ws: InternalWalletState,
   coinPub: string,
@@ -1649,6 +1418,10 @@ class InternalWalletStateImpl implements 
InternalWalletState {
     getMerchantInfo,
   };
 
+  refreshOps: RefreshOperations = {
+    createRefreshGroup,
+  };
+
   // FIXME: Use an LRU cache here.
   private denomCache: Record<string, DenominationInfo> = {};
 

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