gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: wallet-core: refactor peer-pu


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: refactor peer-pull-debit and test aborting
Date: Tue, 09 Jan 2024 16:23:30 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new f8cde03f0 wallet-core: refactor peer-pull-debit and test aborting
f8cde03f0 is described below

commit f8cde03f0cb6a7584fb92885f8979a01916a917d
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Jan 9 16:23:26 2024 +0100

    wallet-core: refactor peer-pull-debit and test aborting
---
 .../src/integrationtests/test-peer-to-peer-pull.ts | 145 +++++-
 packages/taler-util/src/transactions-types.ts      |   4 +-
 packages/taler-wallet-core/src/db.ts               |   4 +
 .../taler-wallet-core/src/operations/common.ts     |  19 +
 .../src/operations/pay-peer-common.ts              |   6 +-
 .../src/operations/pay-peer-pull-debit.ts          | 499 +++++++++------------
 .../taler-wallet-core/src/operations/refresh.ts    |  16 +-
 .../src/operations/transactions.ts                 |  26 +-
 .../taler-wallet-core/src/util/coinSelection.ts    |  12 +-
 packages/taler-wallet-core/src/util/query.ts       |  98 +++-
 packages/taler-wallet-core/tsconfig.json           |   2 +-
 11 files changed, 510 insertions(+), 321 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts 
b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
index 7ed716bc1..a71175407 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -25,10 +25,17 @@ import {
   NotificationType,
   TransactionMajorState,
   TransactionMinorState,
+  TransactionType,
   WalletNotification,
 } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { GlobalTestState } from "../harness/harness.js";
+import {
+  BankServiceHandle,
+  ExchangeService,
+  GlobalTestState,
+  WalletCli,
+  WalletClient,
+} from "../harness/harness.js";
 import {
   createSimpleTestkudosEnvironmentV2,
   createWalletDaemonWithClient,
@@ -65,6 +72,23 @@ export async function runPeerToPeerPullTest(t: 
GlobalTestState) {
   const wallet1 = w1.walletClient;
   const wallet2 = w2.walletClient;
 
+  await checkNormalPeerPull(t, bank, exchange, wallet1, wallet2);
+
+  console.log(`w1 notifications: ${j2s(allW1Notifications)}`);
+
+  // Check that we don't have an excessive number of notifications.
+  t.assertTrue(allW1Notifications.length <= 60);
+
+  await checkAbortedPeerPull(t, bank, exchange, wallet1, wallet2);
+}
+
+async function checkNormalPeerPull(
+  t: GlobalTestState,
+  bank: BankServiceHandle,
+  exchange: ExchangeService,
+  wallet1: WalletClient,
+  wallet2: WalletClient,
+): Promise<void> {
   const withdrawRes = await withdrawViaBankV2(t, {
     walletClient: wallet2,
     bank,
@@ -94,7 +118,8 @@ export async function runPeerToPeerPullTest(t: 
GlobalTestState) {
   );
 
   const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
-    (x) => x.type === NotificationType.TransactionStateTransition &&
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
       x.transactionId === resp.transactionId &&
       x.newTxState.major === TransactionMajorState.Pending &&
       x.newTxState.minor === TransactionMinorState.Ready,
@@ -102,23 +127,32 @@ export async function runPeerToPeerPullTest(t: 
GlobalTestState) {
 
   await peerPullCreditReadyCond;
 
+  const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+    transactionId: resp.transactionId,
+  });
+
+  t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+  t.assertTrue(!!creditTx.talerUri);
+
   const checkResp = await wallet2.client.call(
     WalletApiOperation.PreparePeerPullDebit,
     {
-      talerUri: resp.talerUri,
+      talerUri: creditTx.talerUri,
     },
   );
 
   console.log(`checkResp: ${j2s(checkResp)}`);
 
   const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
-    (x) => x.type === NotificationType.TransactionStateTransition &&
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
       x.transactionId === resp.transactionId &&
       x.newTxState.major === TransactionMajorState.Done,
   );
 
   const peerPullDebitDoneCond = wallet2.waitForNotificationCond(
-    (x) => x.type === NotificationType.TransactionStateTransition &&
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
       x.transactionId === checkResp.transactionId &&
       x.newTxState.major === TransactionMajorState.Done,
   );
@@ -142,11 +176,106 @@ export async function runPeerToPeerPullTest(t: 
GlobalTestState) {
 
   console.log(`txn1: ${j2s(txn1)}`);
   console.log(`txn2: ${j2s(txn2)}`);
+}
 
-  console.log(`w1 notifications: ${j2s(allW1Notifications)}`);
+async function checkAbortedPeerPull(
+  t: GlobalTestState,
+  bank: BankServiceHandle,
+  exchange: ExchangeService,
+  wallet1: WalletClient,
+  wallet2: WalletClient,
+): Promise<void> {
+  const withdrawRes = await withdrawViaBankV2(t, {
+    walletClient: wallet2,
+    bank,
+    exchange,
+    amount: "TESTKUDOS:20",
+  });
 
-  // Check that we don't have an excessive number of notifications.
-  t.assertTrue(allW1Notifications.length <= 60);
+  await withdrawRes.withdrawalFinishedCond;
+
+  const purseExpiration = AbsoluteTime.toProtocolTimestamp(
+    AbsoluteTime.addDuration(
+      AbsoluteTime.now(),
+      Duration.fromSpec({ days: 2 }),
+    ),
+  );
+
+  const resp = await wallet1.client.call(
+    WalletApiOperation.InitiatePeerPullCredit,
+    {
+      exchangeBaseUrl: exchange.baseUrl,
+      partialContractTerms: {
+        summary: "Hello World",
+        amount: "TESTKUDOS:5" as AmountString,
+        purse_expiration: purseExpiration,
+      },
+    },
+  );
+
+  const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
+      x.transactionId === resp.transactionId &&
+      x.newTxState.major === TransactionMajorState.Pending &&
+      x.newTxState.minor === TransactionMinorState.Ready,
+  );
+
+  await peerPullCreditReadyCond;
+
+  const creditTx = await wallet1.call(WalletApiOperation.GetTransactionById, {
+    transactionId: resp.transactionId,
+  });
+
+  t.assertDeepEqual(creditTx.type, TransactionType.PeerPullCredit);
+  t.assertTrue(!!creditTx.talerUri);
+
+  const checkResp = await wallet2.client.call(
+    WalletApiOperation.PreparePeerPullDebit,
+    {
+      talerUri: creditTx.talerUri,
+    },
+  );
+
+  console.log(`checkResp: ${j2s(checkResp)}`);
+
+  const peerPullCreditAbortedCond = wallet1.waitForNotificationCond(
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
+      x.transactionId === resp.transactionId &&
+      x.newTxState.major === TransactionMajorState.Aborted,
+  );
+
+  const peerPullDebitAbortedCond = wallet2.waitForNotificationCond(
+    (x) =>
+      x.type === NotificationType.TransactionStateTransition &&
+      x.transactionId === checkResp.transactionId &&
+      x.newTxState.major === TransactionMajorState.Aborted,
+  );
+
+  await wallet1.call(WalletApiOperation.AbortTransaction, {
+    transactionId: resp.transactionId,
+  });
+
+  await wallet2.client.call(WalletApiOperation.ConfirmPeerPullDebit, {
+    peerPullDebitId: checkResp.peerPullDebitId,
+  });
+
+  await peerPullCreditAbortedCond;
+  await peerPullDebitAbortedCond;
+
+  const txn1 = await wallet1.client.call(
+    WalletApiOperation.GetTransactions,
+    {},
+  );
+
+  const txn2 = await wallet2.client.call(
+    WalletApiOperation.GetTransactions,
+    {},
+  );
+
+  console.log(`txn1: ${j2s(txn1)}`);
+  console.log(`txn2: ${j2s(txn2)}`);
 }
 
 runPeerToPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/transactions-types.ts 
b/packages/taler-util/src/transactions-types.ts
index 740478fb0..17b56d13b 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -369,8 +369,10 @@ export interface TransactionPeerPullCredit extends 
TransactionCommon {
 
   /**
    * URI to send to the other party.
+   *
+   * Only available in the right state.
    */
-  talerUri: string;
+  talerUri: string | undefined;
 }
 
 /**
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 76bb2e393..549bc7517 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -63,7 +63,9 @@ import { DbRetryInfo, TaskIdentifiers } from 
"./operations/common.js";
 import {
   DbAccess,
   DbReadOnlyTransaction,
+  DbReadOnlyTransactionArr,
   DbReadWriteTransaction,
+  DbReadWriteTransactionArr,
   GetReadWriteAccess,
   IndexDescriptor,
   StoreDescriptor,
@@ -2639,6 +2641,8 @@ export const WalletStoresV1 = {
   ),
 };
 
+type WalletStoreNames = StoreNames<typeof WalletStoresV1>;
+
 export type WalletDbReadOnlyTransaction<
   Stores extends StoreNames<typeof WalletStoresV1> & string,
 > = DbReadOnlyTransaction<typeof WalletStoresV1, Stores>;
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index d8fb82be1..1103b7255 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -1075,3 +1075,22 @@ export namespace TaskIdentifiers {
     return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushCreditId}` as 
TaskId;
   }
 }
+
+/**
+ * Result of a transaction transition.
+ */
+export enum TransitionResult {
+  Transition = 1,
+  Stay = 2,
+}
+
+/**
+ * Transaction context.
+ *
+ * FIXME: Should eventually be implemented by all transactions.
+ */
+export interface TransactionContext {
+  abortTransaction(): Promise<void>;
+  resumeTransaction(): Promise<void>;
+  failTransaction(): Promise<void>;
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 1a5dc6e89..88eedb530 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -44,11 +44,7 @@ import { getCandidateWithdrawalDenomsTx } from 
"./withdraw.js";
 const logger = new Logger("operations/peer-to-peer.ts");
 
 /**
- * Get information about the coin selected for signatures
- *
- * @param ws
- * @param csel
- * @returns
+ * Get information about the coin selected for signatures.
  */
 export async function queryCoinInfosForSelection(
   ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 72e9e2e4a..9bbe2c875 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2022-2023 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -14,6 +14,15 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+/**
+ * @fileoverview
+ * Implementation of the peer-pull-debit transaction, i.e.
+ * paying for an invoice the wallet received from another wallet.
+ */
+
+/**
+ * Imports.
+ */
 import {
   AcceptPeerPullPaymentResponse,
   Amounts,
@@ -53,19 +62,25 @@ import {
   readTalerErrorResponse,
 } from "@gnu-taler/taler-util/http";
 import {
+  DbReadWriteTransactionArr,
   InternalWalletState,
   PeerPullDebitRecordStatus,
   PeerPullPaymentIncomingRecord,
   PendingTaskType,
   RefreshOperationStatus,
+  StoreNames,
+  WalletStoresV1,
   createRefreshGroup,
   timestampPreciseToDb,
 } from "../index.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
 import { checkLogicInvariant } from "../util/invariants.js";
 import {
   TaskRunResult,
   TaskRunResultType,
+  TransactionContext,
+  TransitionResult,
   constructTaskIdentifier,
   spendCoins,
 } from "./common.js";
@@ -80,19 +95,181 @@ import {
   parseTransactionIdentifier,
   stopLongpolling,
 } from "./transactions.js";
-import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
 
 const logger = new Logger("pay-peer-pull-debit.ts");
 
+/**
+ * Common context for a peer-pull-debit transaction.
+ */
+export class PeerPullDebitTransactionContext implements TransactionContext {
+  ws: InternalWalletState;
+  transactionId: string;
+  taskId: string;
+  peerPullDebitId: string;
+
+  constructor(ws: InternalWalletState, peerPullDebitId: string) {
+    this.ws = ws;
+    this.transactionId = constructTransactionIdentifier({
+      tag: TransactionType.PeerPullDebit,
+      peerPullDebitId,
+    });
+    this.taskId = constructTaskIdentifier({
+      tag: PendingTaskType.PeerPullDebit,
+      peerPullDebitId,
+    });
+    this.peerPullDebitId = peerPullDebitId;
+  }
+
+  async resumeTransaction(): Promise<void> {
+    const ctx = this;
+    stopLongpolling(ctx.ws, ctx.taskId);
+    await ctx.transition(async (pi) => {
+      switch (pi.status) {
+        case PeerPullDebitRecordStatus.SuspendedDeposit:
+          pi.status = PeerPullDebitRecordStatus.PendingDeposit;
+          return TransitionResult.Transition;
+        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+          pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+          return TransitionResult.Transition;
+        case PeerPullDebitRecordStatus.Aborted:
+        case PeerPullDebitRecordStatus.AbortingRefresh:
+        case PeerPullDebitRecordStatus.Failed:
+        case PeerPullDebitRecordStatus.DialogProposed:
+        case PeerPullDebitRecordStatus.Done:
+        case PeerPullDebitRecordStatus.PendingDeposit:
+          return TransitionResult.Stay;
+      }
+    });
+  }
+
+  async failTransaction(): Promise<void> {
+    const ctx = this;
+    stopLongpolling(ctx.ws, ctx.taskId);
+    await ctx.transition(async (pi) => {
+      switch (pi.status) {
+        case PeerPullDebitRecordStatus.SuspendedDeposit:
+        case PeerPullDebitRecordStatus.PendingDeposit:
+        case PeerPullDebitRecordStatus.AbortingRefresh:
+        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
+          // FIXME: Should we also abort the corresponding refresh session?!
+          pi.status = PeerPullDebitRecordStatus.Failed;
+          return TransitionResult.Transition;
+        default:
+          return TransitionResult.Stay;
+      }
+    });
+  }
+
+  async abortTransaction(): Promise<void> {
+    const ctx = this;
+    await ctx.transitionExtra(
+      {
+        extraStores: [
+          "coinAvailability",
+          "denominations",
+          "refreshGroups",
+          "coins",
+          "coinAvailability",
+        ],
+      },
+      async (pi, tx) => {
+        switch (pi.status) {
+          case PeerPullDebitRecordStatus.SuspendedDeposit:
+          case PeerPullDebitRecordStatus.PendingDeposit:
+            break;
+          default:
+            return TransitionResult.Stay;
+        }
+        const currency = Amounts.currencyOf(pi.totalCostEstimated);
+        const coinPubs: CoinRefreshRequest[] = [];
+
+        if (!pi.coinSel) {
+          throw Error("invalid db state");
+        }
+
+        for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
+          coinPubs.push({
+            amount: pi.coinSel.contributions[i],
+            coinPub: pi.coinSel.coinPubs[i],
+          });
+        }
+
+        const refresh = await createRefreshGroup(
+          ctx.ws,
+          tx,
+          currency,
+          coinPubs,
+          RefreshReason.AbortPeerPullDebit,
+        );
+
+        pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
+        pi.abortRefreshGroupId = refresh.refreshGroupId;
+        return TransitionResult.Transition;
+      },
+    );
+  }
+
+  async transition(
+    f: (rec: PeerPullPaymentIncomingRecord) => Promise<TransitionResult>,
+  ): Promise<void> {
+    return this.transitionExtra(
+      {
+        extraStores: [],
+      },
+      f,
+    );
+  }
+
+  async transitionExtra<
+    StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [],
+  >(
+    opts: { extraStores: StoreNameArray },
+    f: (
+      rec: PeerPullPaymentIncomingRecord,
+      tx: DbReadWriteTransactionArr<
+        typeof WalletStoresV1,
+        ["peerPullDebit", ...StoreNameArray]
+      >,
+    ) => Promise<TransitionResult>,
+  ): Promise<void> {
+    const ws = this.ws;
+    const extraStores = opts.extraStores ?? [];
+    const transitionInfo = await ws.db.runReadWriteTx(
+      ["peerPullDebit", ...extraStores],
+      async (tx) => {
+        const pi = await tx.peerPullDebit.get(this.peerPullDebitId);
+        if (!pi) {
+          throw Error("peer pull payment not found anymore");
+        }
+        const oldTxState = computePeerPullDebitTransactionState(pi);
+        const res = await f(pi, tx);
+        switch (res) {
+          case TransitionResult.Transition: {
+            await tx.peerPullDebit.put(pi);
+            const newTxState = computePeerPullDebitTransactionState(pi);
+            return {
+              oldTxState,
+              newTxState,
+            };
+          }
+          default:
+            return undefined;
+        }
+      },
+    );
+    notifyTransition(ws, this.transactionId, transitionInfo);
+  }
+}
+
 async function handlePurseCreationConflict(
-  ws: InternalWalletState,
+  ctx: PeerPullDebitTransactionContext,
   peerPullInc: PeerPullPaymentIncomingRecord,
   resp: HttpResponse,
 ): Promise<TaskRunResult> {
-  const pursePub = peerPullInc.pursePub;
+  const ws = ctx.ws;
   const errResp = await readTalerErrorResponse(resp);
   if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
-    await failPeerPullDebitTransaction(ws, pursePub);
+    await ctx.failTransaction();
     return TaskRunResult.finished();
   }
 
@@ -139,29 +316,27 @@ async function handlePurseCreationConflict(
     coinSelRes.result.coins,
   );
 
-  await ws.db
-    .mktx((x) => [x.peerPullDebit])
-    .runReadWrite(async (tx) => {
-      const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
-      if (!myPpi) {
-        return;
-      }
-      switch (myPpi.status) {
-        case PeerPullDebitRecordStatus.PendingDeposit:
-        case PeerPullDebitRecordStatus.SuspendedDeposit: {
-          const sel = coinSelRes.result;
-          myPpi.coinSel = {
-            coinPubs: sel.coins.map((x) => x.coinPub),
-            contributions: sel.coins.map((x) => x.contribution),
-            totalCost: Amounts.stringify(totalAmount),
-          };
-          break;
-        }
-        default:
-          return;
+  await ws.db.runReadWriteTx(["peerPullDebit"], async (tx) => {
+    const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
+    if (!myPpi) {
+      return;
+    }
+    switch (myPpi.status) {
+      case PeerPullDebitRecordStatus.PendingDeposit:
+      case PeerPullDebitRecordStatus.SuspendedDeposit: {
+        const sel = coinSelRes.result;
+        myPpi.coinSel = {
+          coinPubs: sel.coins.map((x) => x.coinPub),
+          contributions: sel.coins.map((x) => x.contribution),
+          totalCost: Amounts.stringify(totalAmount),
+        };
+        break;
       }
-      await tx.peerPullDebit.put(myPpi);
-    });
+      default:
+        return;
+    }
+    await tx.peerPullDebit.put(myPpi);
+  });
   return TaskRunResult.finished();
 }
 
@@ -169,7 +344,6 @@ async function processPeerPullDebitPendingDeposit(
   ws: InternalWalletState,
   peerPullInc: PeerPullPaymentIncomingRecord,
 ): Promise<TaskRunResult> {
-  const peerPullDebitId = peerPullInc.peerPullDebitId;
   const pursePub = peerPullInc.pursePub;
 
   const coinSel = peerPullInc.coinSel;
@@ -198,15 +372,16 @@ async function processPeerPullDebitPendingDeposit(
     logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
   }
 
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullDebit,
-    peerPullDebitId,
-  });
-
   const httpResp = await ws.http.fetch(purseDepositUrl.href, {
     method: "POST",
     body: depositPayload,
   });
+
+  const ctx = new PeerPullDebitTransactionContext(
+    ws,
+    peerPullInc.peerPullDebitId,
+  );
+
   switch (httpResp.status) {
     case HttpStatusCode.Ok: {
       const resp = await readSuccessResponseJsonOrThrow(
@@ -215,77 +390,21 @@ async function processPeerPullDebitPendingDeposit(
       );
       logger.trace(`purse deposit response: ${j2s(resp)}`);
 
-      const transitionInfo = await ws.db
-        .mktx((x) => [x.peerPullDebit])
-        .runReadWrite(async (tx) => {
-          const pi = await tx.peerPullDebit.get(peerPullDebitId);
-          if (!pi) {
-            throw Error("peer pull payment not found anymore");
-          }
-          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
-            return;
-          }
-          const oldTxState = computePeerPullDebitTransactionState(pi);
-          pi.status = PeerPullDebitRecordStatus.Done;
-          const newTxState = computePeerPullDebitTransactionState(pi);
-          await tx.peerPullDebit.put(pi);
-          return { oldTxState, newTxState };
-        });
-      notifyTransition(ws, transactionId, transitionInfo);
-      break;
+      await ctx.transition(async (r) => {
+        if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) {
+          return TransitionResult.Stay;
+        }
+        r.status = PeerPullDebitRecordStatus.Done;
+        return TransitionResult.Transition;
+      });
+      return TaskRunResult.finished();
     }
     case HttpStatusCode.Gone: {
-      const transitionInfo = await ws.db
-        .mktx((x) => [
-          x.peerPullDebit,
-          x.refreshGroups,
-          x.denominations,
-          x.coinAvailability,
-          x.coins,
-        ])
-        .runReadWrite(async (tx) => {
-          const pi = await tx.peerPullDebit.get(peerPullDebitId);
-          if (!pi) {
-            throw Error("peer pull payment not found anymore");
-          }
-          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
-            return;
-          }
-          const oldTxState = computePeerPullDebitTransactionState(pi);
-
-          const currency = Amounts.currencyOf(pi.totalCostEstimated);
-          const coinPubs: CoinRefreshRequest[] = [];
-
-          if (!pi.coinSel) {
-            throw Error("invalid db state");
-          }
-
-          for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
-            coinPubs.push({
-              amount: pi.coinSel.contributions[i],
-              coinPub: pi.coinSel.coinPubs[i],
-            });
-          }
-
-          const refresh = await createRefreshGroup(
-            ws,
-            tx,
-            currency,
-            coinPubs,
-            RefreshReason.AbortPeerPullDebit,
-          );
-
-          pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
-          pi.abortRefreshGroupId = refresh.refreshGroupId;
-          const newTxState = computePeerPullDebitTransactionState(pi);
-          await tx.peerPullDebit.put(pi);
-          return { oldTxState, newTxState };
-        });
-      notifyTransition(ws, transactionId, transitionInfo);
-      break;
+      await ctx.abortTransaction();
+      return TaskRunResult.finished();
     }
     case HttpStatusCode.Conflict: {
-      return handlePurseCreationConflict(ws, peerPullInc, httpResp);
+      return handlePurseCreationConflict(ctx, peerPullInc, httpResp);
     }
     default: {
       const errResp = await readTalerErrorResponse(httpResp);
@@ -295,7 +414,6 @@ async function processPeerPullDebitPendingDeposit(
       };
     }
   }
-  return TaskRunResult.finished();
 }
 
 async function processPeerPullDebitAbortingRefresh(
@@ -624,6 +742,9 @@ export async function preparePeerPullDebit(
   };
 }
 
+/**
+ * FIXME: This belongs in the transaction context!
+ */
 export async function suspendPeerPullDebitTransaction(
   ws: InternalWalletState,
   peerPullDebitId: string,
@@ -683,182 +804,6 @@ export async function suspendPeerPullDebitTransaction(
   notifyTransition(ws, transactionId, transitionInfo);
 }
 
-export async function abortPeerPullDebitTransaction(
-  ws: InternalWalletState,
-  peerPullDebitId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullDebit])
-    .runReadWrite(async (tx) => {
-      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
-      if (!pullDebitRec) {
-        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
-        return;
-      }
-      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
-      switch (pullDebitRec.status) {
-        case PeerPullDebitRecordStatus.DialogProposed:
-          newStatus = PeerPullDebitRecordStatus.Aborted;
-          break;
-        case PeerPullDebitRecordStatus.Done:
-          break;
-        case PeerPullDebitRecordStatus.PendingDeposit:
-          newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
-          break;
-        case PeerPullDebitRecordStatus.SuspendedDeposit:
-          break;
-        case PeerPullDebitRecordStatus.Aborted:
-          break;
-        case PeerPullDebitRecordStatus.AbortingRefresh:
-          break;
-        case PeerPullDebitRecordStatus.Failed:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
-          break;
-        default:
-          assertUnreachable(pullDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        pullDebitRec.status = newStatus;
-        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        await tx.peerPullDebit.put(pullDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function failPeerPullDebitTransaction(
-  ws: InternalWalletState,
-  peerPullDebitId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullDebit])
-    .runReadWrite(async (tx) => {
-      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
-      if (!pullDebitRec) {
-        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
-        return;
-      }
-      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
-      switch (pullDebitRec.status) {
-        case PeerPullDebitRecordStatus.DialogProposed:
-          newStatus = PeerPullDebitRecordStatus.Aborted;
-          break;
-        case PeerPullDebitRecordStatus.Done:
-          break;
-        case PeerPullDebitRecordStatus.PendingDeposit:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedDeposit:
-          break;
-        case PeerPullDebitRecordStatus.Aborted:
-          break;
-        case PeerPullDebitRecordStatus.Failed:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
-        case PeerPullDebitRecordStatus.AbortingRefresh:
-          // FIXME: abort underlying refresh!
-          newStatus = PeerPullDebitRecordStatus.Failed;
-          break;
-        default:
-          assertUnreachable(pullDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        pullDebitRec.status = newStatus;
-        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        await tx.peerPullDebit.put(pullDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
-export async function resumePeerPullDebitTransaction(
-  ws: InternalWalletState,
-  peerPullDebitId: string,
-) {
-  const taskId = constructTaskIdentifier({
-    tag: PendingTaskType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  const transactionId = constructTransactionIdentifier({
-    tag: TransactionType.PeerPullDebit,
-    peerPullDebitId,
-  });
-  stopLongpolling(ws, taskId);
-  const transitionInfo = await ws.db
-    .mktx((x) => [x.peerPullDebit])
-    .runReadWrite(async (tx) => {
-      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
-      if (!pullDebitRec) {
-        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
-        return;
-      }
-      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
-      switch (pullDebitRec.status) {
-        case PeerPullDebitRecordStatus.DialogProposed:
-        case PeerPullDebitRecordStatus.Done:
-        case PeerPullDebitRecordStatus.PendingDeposit:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedDeposit:
-          newStatus = PeerPullDebitRecordStatus.PendingDeposit;
-          break;
-        case PeerPullDebitRecordStatus.Aborted:
-          break;
-        case PeerPullDebitRecordStatus.AbortingRefresh:
-          break;
-        case PeerPullDebitRecordStatus.Failed:
-          break;
-        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
-          newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
-          break;
-        default:
-          assertUnreachable(pullDebitRec.status);
-      }
-      if (newStatus != null) {
-        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        pullDebitRec.status = newStatus;
-        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
-        await tx.peerPullDebit.put(pullDebitRec);
-        return {
-          oldTxState,
-          newTxState,
-        };
-      }
-      return undefined;
-    });
-  ws.workAvailable.trigger();
-  notifyTransition(ws, transactionId, transitionInfo);
-}
-
 export function computePeerPullDebitTransactionState(
   pullDebitRecord: PeerPullPaymentIncomingRecord,
 ): TransactionState {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 17ac54cfb..a8bcb28d1 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -84,6 +84,7 @@ import {
   RefreshSessionRecord,
   timestampPreciseToDb,
   timestampProtocolFromDb,
+  WalletDbReadWriteTransaction,
 } from "../index.js";
 import {
   EXCHANGE_COINS_LOCK,
@@ -92,7 +93,11 @@ import {
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import { selectWithdrawalDenominations } from "../util/coinSelection.js";
 import { checkDbInvariant } from "../util/invariants.js";
-import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
+import {
+  DbReadWriteTransaction,
+  GetReadOnlyAccess,
+  GetReadWriteAccess,
+} from "../util/query.js";
 import {
   constructTaskIdentifier,
   makeCoinAvailable,
@@ -1097,12 +1102,9 @@ async function applyRefresh(
  */
 export async function createRefreshGroup(
   ws: InternalWalletState,
-  tx: GetReadWriteAccess<{
-    denominations: typeof WalletStoresV1.denominations;
-    coins: typeof WalletStoresV1.coins;
-    refreshGroups: typeof WalletStoresV1.refreshGroups;
-    coinAvailability: typeof WalletStoresV1.coinAvailability;
-  }>,
+  tx: WalletDbReadWriteTransaction<
+    "denominations" | "coins" | "refreshGroups" | "coinAvailability"
+  >,
   currency: string,
   oldCoinPubs: CoinRefreshRequest[],
   reason: RefreshReason,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 3a219b39b..142eff7c1 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -114,11 +114,9 @@ import {
   suspendPeerPullCreditTransaction,
 } from "./pay-peer-pull-credit.js";
 import {
-  abortPeerPullDebitTransaction,
   computePeerPullDebitTransactionActions,
   computePeerPullDebitTransactionState,
-  failPeerPullDebitTransaction,
-  resumePeerPullDebitTransaction,
+  PeerPullDebitTransactionContext,
   suspendPeerPullDebitTransaction,
 } from "./pay-peer-pull-debit.js";
 import {
@@ -1647,9 +1645,11 @@ export async function failTransaction(
     case TransactionType.PeerPullCredit:
       await failPeerPullCreditTransaction(ws, tx.pursePub);
       return;
-    case TransactionType.PeerPullDebit:
-      await failPeerPullDebitTransaction(ws, tx.peerPullDebitId);
+    case TransactionType.PeerPullDebit: {
+      const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
+      await ctx.failTransaction();
       return;
+    }
     case TransactionType.PeerPushCredit:
       await failPeerPushCreditTransaction(ws, tx.peerPushCreditId);
       return;
@@ -1692,9 +1692,11 @@ export async function resumeTransaction(
     case TransactionType.PeerPushDebit:
       await resumePeerPushDebitTransaction(ws, tx.pursePub);
       break;
-    case TransactionType.PeerPullDebit:
-      await resumePeerPullDebitTransaction(ws, tx.peerPullDebitId);
-      break;
+    case TransactionType.PeerPullDebit: {
+      const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId);
+      await ctx.resumeTransaction();
+      return;
+    }
     case TransactionType.PeerPushCredit:
       await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId);
       break;
@@ -1936,9 +1938,11 @@ export async function abortTransaction(
     case TransactionType.PeerPullCredit:
       await abortPeerPullCreditTransaction(ws, txId.pursePub);
       break;
-    case TransactionType.PeerPullDebit:
-      await abortPeerPullDebitTransaction(ws, txId.peerPullDebitId);
-      break;
+    case TransactionType.PeerPullDebit: {
+      const ctx = new PeerPullDebitTransactionContext(ws, 
txId.peerPullDebitId);
+      await ctx.abortTransaction();
+      return;
+    }
     case TransactionType.PeerPushCredit:
       await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId);
       break;
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 6070f4c78..9b29cee26 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -233,9 +233,9 @@ function tallyFees(
 
 export type SelectPayCoinsResult =
   | {
-    type: "failure";
-    insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
-  }
+      type: "failure";
+      insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
+    }
   | { type: "success"; coinSel: PayCoinSelection };
 
 /**
@@ -889,9 +889,9 @@ export interface PeerCoinSelectionDetails {
 export type SelectPeerCoinsResult =
   | { type: "success"; result: PeerCoinSelectionDetails }
   | {
-    type: "failure";
-    insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
-  };
+      type: "failure";
+      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+    };
 
 export interface PeerCoinRepair {
   exchangeBaseUrl: string;
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 309c17a43..5d563f620 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -454,8 +454,8 @@ type DerefKeyPath<T, P> = P extends `${infer PX extends 
keyof T &
   KeyPathComponents}`
   ? T[PX]
   : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
-  ? DerefKeyPath<T[P0], Rest>
-  : unknown;
+    ? DerefKeyPath<T[P0], Rest>
+    : unknown;
 
 /**
  * Return a path if it is a valid dot-separate path to an object.
@@ -465,8 +465,8 @@ type ValidateKeyPath<T, P> = P extends `${infer PX extends 
keyof T &
   KeyPathComponents}`
   ? PX
   : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}`
-  ? `${P0}.${ValidateKeyPath<T[P0], Rest>}`
-  : never;
+    ? `${P0}.${ValidateKeyPath<T[P0], Rest>}`
+    : never;
 
 // function foo<T, P>(
 //   x: T,
@@ -545,11 +545,60 @@ type ReadWriteTransactionFunction<BoundStores, T> = (
   rawTx: IDBTransaction,
 ) => Promise<T>;
 
+export type DbReadWriteTransactionArr<
+  StoreMap,
+  StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+  [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+  ? {
+      [X in StoresArr[number] &
+        keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+        infer _StoreName,
+        infer RecordType,
+        infer IndexMap
+      >
+        ? StoreReadWriteAccessor<RecordType, IndexMap>
+        : unknown;
+    }
+  : never;
+
+export type DbReadOnlyTransactionArr<
+  StoreMap,
+  StoresArr extends Array<StoreNames<StoreMap>>,
+> = StoreMap extends {
+  [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>;
+}
+  ? {
+      [X in StoresArr[number] &
+        keyof StoreMap]: StoreMap[X] extends StoreWithIndexes<
+        infer _StoreName,
+        infer RecordType,
+        infer IndexMap
+      >
+        ? StoreReadOnlyAccessor<RecordType, IndexMap>
+        : unknown;
+    }
+  : never;
+
 export interface TransactionContext<BoundStores> {
   runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
   runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
 }
 
+/**
+ * Convert the type of an array to a union of the contents.
+ *
+ * Example:
+ * Input ["foo", "bar"]
+ * Output "foo" | "bar"
+ */
+export type UnionFromArray<Arr> = Arr extends {
+  [X in keyof Arr]: Arr[X] & string;
+}
+  ? Arr[keyof Arr & number]
+  : unknown;
+
 function runTx<Arg, Res>(
   tx: IDBTransaction,
   arg: Arg,
@@ -743,7 +792,10 @@ type StoreNamesOf<X> = X extends { [x: number]: infer F }
  * A store map is the metadata that describes the store.
  */
 export class DbAccess<StoreMap> {
-  constructor(private db: IDBDatabase, private stores: StoreMap) {}
+  constructor(
+    private db: IDBDatabase,
+    private stores: StoreMap,
+  ) {}
 
   idbHandle(): IDBDatabase {
     return this.db;
@@ -803,6 +855,42 @@ export class DbAccess<StoreMap> {
     };
   }
 
+  runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+    storeNames: StoreNameArray,
+    txf: (
+      tx: DbReadWriteTransactionArr<StoreMap, StoreNameArray>,
+    ) => Promise<T>,
+  ): Promise<T> {
+    const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+      {};
+    const strStoreNames: string[] = [];
+    for (const sn of storeNames) {
+      const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+      strStoreNames.push(swi.storeName);
+      accessibleStores[swi.storeName] = swi;
+    }
+    const tx = this.db.transaction(strStoreNames, "readwrite");
+    const writeContext = makeWriteContext(tx, accessibleStores);
+    return runTx(tx, writeContext, txf);
+  }
+
+  runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>(
+    storeNames: StoreNameArray,
+    txf: (tx: DbReadOnlyTransactionArr<StoreMap, StoreNameArray>) => 
Promise<T>,
+  ): Promise<T> {
+    const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } =
+      {};
+    const strStoreNames: string[] = [];
+    for (const sn of storeNames) {
+      const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>;
+      strStoreNames.push(swi.storeName);
+      accessibleStores[swi.storeName] = swi;
+    }
+    const tx = this.db.transaction(strStoreNames, "readwrite");
+    const readContext = makeReadContext(tx, accessibleStores);
+    return runTx(tx, readContext, txf);
+  }
+
   /**
    * Run a transaction with selected object stores.
    *
diff --git a/packages/taler-wallet-core/tsconfig.json 
b/packages/taler-wallet-core/tsconfig.json
index 663a4dd98..7369e9783 100644
--- a/packages/taler-wallet-core/tsconfig.json
+++ b/packages/taler-wallet-core/tsconfig.json
@@ -15,7 +15,7 @@
     "noImplicitReturns": true,
     "noFallthroughCasesInSwitch": true,
     "strict": true,
-    "strictPropertyInitialization": false,
+    "strictPropertyInitialization": true,
     "outDir": "lib",
     "noImplicitAny": true,
     "noImplicitThis": true,

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