gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (bc3e40310 -> 65a656163)


From: gnunet
Subject: [taler-wallet-core] branch master updated (bc3e40310 -> 65a656163)
Date: Wed, 03 Apr 2024 16:21:36 +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 bc3e40310 wallet-core: don't throw on re-initialization
     new 5417b8b7b wallet-core: preparations for deferred coin selection
     new 65a656163 wallet-core: allow deposits with balance locked behind 
refresh

The 2 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:
 ...h-blocked.ts => test-wallet-blocked-deposit.ts} |  50 ++-
 .../src/integrationtests/testrunner.ts             |   4 +-
 packages/taler-util/src/wallet-types.ts            |  34 +-
 packages/taler-wallet-core/src/coinSelection.ts    | 400 +++++++++++++++------
 packages/taler-wallet-core/src/db.ts               |   6 +-
 packages/taler-wallet-core/src/deposits.ts         | 321 +++++++++++------
 packages/taler-wallet-core/src/dev-experiments.ts  |  96 ++---
 packages/taler-wallet-core/src/pay-merchant.ts     | 106 +++---
 .../taler-wallet-core/src/pay-peer-pull-debit.ts   |  79 ++--
 .../taler-wallet-core/src/pay-peer-push-debit.ts   |  75 ++--
 packages/taler-wallet-core/src/transactions.ts     |  29 +-
 packages/taler-wallet-core/src/wallet.ts           |   4 +-
 12 files changed, 833 insertions(+), 371 deletions(-)
 rename 
packages/taler-harness/src/integrationtests/{test-wallet-refresh-blocked.ts => 
test-wallet-blocked-deposit.ts} (67%)

diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts 
b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
similarity index 67%
rename from 
packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts
rename to 
packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
index 8c568d190..cb9c54f1d 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts
@@ -17,10 +17,16 @@
 /**
  * Imports.
  */
-import { j2s } from "@gnu-taler/taler-util";
+import {
+  AmountString,
+  NotificationType,
+  TransactionMajorState,
+  TransactionMinorState,
+  j2s,
+} from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 import { CoinConfig } from "../harness/denomStructures.js";
-import { GlobalTestState } from "../harness/harness.js";
+import { GlobalTestState, generateRandomPayto } from "../harness/harness.js";
 import {
   createSimpleTestkudosEnvironmentV2,
   createWalletDaemonWithClient,
@@ -43,7 +49,7 @@ const coinCommon = {
 /**
  * Run test for refreshe after a payment.
  */
-export async function runWalletRefreshBlockedTest(t: GlobalTestState) {
+export async function runWalletBlockedDeposit(t: GlobalTestState) {
   // Set up test environment
 
   const coinConfigList: CoinConfig[] = [
@@ -66,6 +72,7 @@ export async function runWalletRefreshBlockedTest(t: 
GlobalTestState) {
 
   const { walletClient: w1 } = await createWalletDaemonWithClient(t, {
     name: "w1",
+    persistent: true,
     config: {
       testing: {
         devModeActive: true,
@@ -97,6 +104,8 @@ export async function runWalletRefreshBlockedTest(t: 
GlobalTestState) {
     },
   });
 
+  const userPayto = generateRandomPayto("foo");
+
   const bal = await w1.call(WalletApiOperation.GetBalances, {});
   console.log(`balance: ${j2s(bal)}`);
 
@@ -106,7 +115,38 @@ export async function runWalletRefreshBlockedTest(t: 
GlobalTestState) {
   console.log(`balance details: ${j2s(balDet)}`);
 
   // FIXME: Now check deposit/p2p/pay
-  t.assertTrue(false);
+
+  const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, {
+    amount: "TESTKUDOS:18" as AmountString,
+    depositPaytoUri: userPayto,
+  });
+
+  console.log(`check resp: ${j2s(depositCheckResp)}`);
+
+  const depositCreateResp = await w1.call(
+    WalletApiOperation.CreateDepositGroup,
+    {
+      amount: "TESTKUDOS:18" as AmountString,
+      depositPaytoUri: userPayto,
+    },
+  );
+
+  console.log(`create resp: ${j2s(depositCreateResp)}`);
+
+  const depositTrackCond = w1.waitForNotificationCond((n) => {
+    return (
+      n.type === NotificationType.TransactionStateTransition &&
+      n.transactionId === depositCreateResp.transactionId &&
+      n.newTxState.major === TransactionMajorState.Pending &&
+      n.newTxState.minor === TransactionMinorState.Track
+    );
+  });
+
+  await w1.call(WalletApiOperation.ApplyDevExperiment, {
+    devExperimentUri: "taler://dev-experiment/stop-block-refresh",
+  });
+
+  await depositTrackCond;
 }
 
-runWalletRefreshBlockedTest.suites = ["wallet"];
+runWalletBlockedDeposit.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts 
b/packages/taler-harness/src/integrationtests/testrunner.ts
index 2bca91e45..063aefa43 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -101,7 +101,7 @@ import { runWalletGenDbTest } from "./test-wallet-gendb.js";
 import { runWalletInsufficientBalanceTest } from 
"./test-wallet-insufficient-balance.js";
 import { runWalletNotificationsTest } from "./test-wallet-notifications.js";
 import { runWalletObservabilityTest } from "./test-wallet-observability.js";
-import { runWalletRefreshBlockedTest } from "./test-wallet-refresh-blocked.js";
+import { runWalletBlockedDeposit } from "./test-wallet-blocked-deposit.js";
 import { runWalletRefreshTest } from "./test-wallet-refresh.js";
 import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
 import { runWallettestingTest } from "./test-wallettesting.js";
@@ -213,7 +213,7 @@ const allTests: TestMainFunction[] = [
   runWalletWirefeesTest,
   runDenomLostTest,
   runWalletDenomExpireTest,
-  runWalletRefreshBlockedTest,
+  runWalletBlockedDeposit,
 ];
 
 export interface TestRunSpec {
diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index e5eb618f0..441da7a87 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -633,11 +633,11 @@ export interface CoinDumpJson {
     withdrawal_reserve_pub: string | undefined;
     coin_status: CoinStatus;
     spend_allocation:
-    | {
-      id: string;
-      amount: AmountString;
-    }
-    | undefined;
+      | {
+          id: string;
+          amount: AmountString;
+        }
+      | undefined;
     /**
      * Information about the age restriction
      */
@@ -836,7 +836,7 @@ export const codecForPreparePayResultPaymentPossible =
       )
       .build("PreparePayResultPaymentPossible");
 
-export interface BalanceDetails { }
+export interface BalanceDetails {}
 
 /**
  * Detailed reason for why the wallet's balance is insufficient.
@@ -2662,8 +2662,16 @@ export interface TestPayResult {
 }
 
 export interface SelectedCoin {
+  denomPubHash: string;
   coinPub: string;
   contribution: AmountString;
+  exchangeBaseUrl: string;
+}
+
+export interface SelectedProspectiveCoin {
+  denomPubHash: string;
+  contribution: AmountString;
+  exchangeBaseUrl: string;
 }
 
 /**
@@ -2684,6 +2692,20 @@ export interface PayCoinSelection {
   customerDepositFees: AmountString;
 }
 
+export interface ProspectivePayCoinSelection {
+  prospectiveCoins: SelectedProspectiveCoin[];
+
+  /**
+   * How much of the wire fees is the customer paying?
+   */
+  customerWireFees: AmountString;
+
+  /**
+   * How much of the deposit fees is the customer paying?
+   */
+  customerDepositFees: AmountString;
+}
+
 export interface CheckPeerPushDebitRequest {
   /**
    * Preferred exchange to use for the p2p payment.
diff --git a/packages/taler-wallet-core/src/coinSelection.ts 
b/packages/taler-wallet-core/src/coinSelection.ts
index 6e3ef5917..bce51fd91 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -44,7 +44,9 @@ import {
   parsePaytoUri,
   PayCoinSelection,
   PaymentInsufficientBalanceDetails,
+  ProspectivePayCoinSelection,
   SelectedCoin,
+  SelectedProspectiveCoin,
   strcmp,
   TalerProtocolTimestamp,
 } from "@gnu-taler/taler-util";
@@ -158,8 +160,101 @@ export type SelectPayCoinsResult =
       type: "failure";
       insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
     }
+  | { type: "prospective"; result: ProspectivePayCoinSelection }
   | { type: "success"; coinSel: PayCoinSelection };
 
+async function internalSelectPayCoins(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<
+    [
+      "coinAvailability",
+      "denominations",
+      "refreshGroups",
+      "exchanges",
+      "exchangeDetails",
+      "coins",
+    ]
+  >,
+  req: SelectPayCoinRequestNg,
+  includePendingCoins: boolean,
+): Promise<
+  | { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally }
+  | undefined
+> {
+  const { contractTermsAmount, depositFeeLimit } = req;
+  const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
+    wex,
+    tx,
+    {
+      restrictExchanges: req.restrictExchanges,
+      instructedAmount: req.contractTermsAmount,
+      restrictWireMethod: req.restrictWireMethod,
+      depositPaytoUri: req.depositPaytoUri,
+      requiredMinimumAge: req.requiredMinimumAge,
+      includePendingCoins,
+    },
+  );
+
+  if (logger.shouldLogTrace()) {
+    logger.trace(
+      `instructed amount: ${Amounts.stringify(req.contractTermsAmount)}`,
+    );
+    logger.trace(`wire fees per exchange: ${j2s(wireFeesPerExchange)}`);
+    logger.trace(`candidates: ${j2s(candidateDenoms)}`);
+  }
+
+  const coinRes: SelectedCoin[] = [];
+  const currency = contractTermsAmount.currency;
+
+  let tally: CoinSelectionTally = {
+    amountPayRemaining: contractTermsAmount,
+    amountDepositFeeLimitRemaining: depositFeeLimit,
+    customerDepositFees: Amounts.zeroOfCurrency(currency),
+    customerWireFees: Amounts.zeroOfCurrency(currency),
+    wireFeeCoveredForExchange: new Set(),
+    lastDepositFee: Amounts.zeroOfCurrency(currency),
+  };
+
+  await maybeRepairCoinSelection(
+    wex,
+    tx,
+    req.prevPayCoins ?? [],
+    coinRes,
+    tally,
+    {
+      wireFeeAmortization: req.wireFeeAmortization,
+      wireFeesPerExchange: wireFeesPerExchange,
+    },
+  );
+
+  let selectedDenom: SelResult | undefined;
+  if (req.forcedSelection) {
+    selectedDenom = selectForced(req, candidateDenoms);
+  } else {
+    // FIXME:  Here, we should select coins in a smarter way.
+    // Instead of always spending the next-largest coin,
+    // we should try to find the smallest coin that covers the
+    // amount.
+    selectedDenom = selectGreedy(
+      {
+        wireFeeAmortization: req.wireFeeAmortization,
+        wireFeesPerExchange: wireFeesPerExchange,
+      },
+      candidateDenoms,
+      tally,
+    );
+  }
+
+  if (!selectedDenom) {
+    return undefined;
+  }
+  return {
+    sel: selectedDenom,
+    coinRes,
+    tally,
+  };
+}
+
 /**
  * Select coins to spend under the merchant's constraints.
  *
@@ -171,8 +266,6 @@ export async function selectPayCoins(
   wex: WalletExecutionContext,
   req: SelectPayCoinRequestNg,
 ): Promise<SelectPayCoinsResult> {
-  const { contractTermsAmount, depositFeeLimit } = req;
-
   if (logger.shouldLogTrace()) {
     logger.trace(`selecting coins for ${j2s(req)}`);
   }
@@ -187,69 +280,42 @@ export async function selectPayCoins(
       "coins",
     ],
     async (tx) => {
-      const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
-        wex,
-        tx,
-        {
-          restrictExchanges: req.restrictExchanges,
-          instructedAmount: req.contractTermsAmount,
-          restrictWireMethod: req.restrictWireMethod,
-          depositPaytoUri: req.depositPaytoUri,
-          requiredMinimumAge: req.requiredMinimumAge,
-        },
-      );
+      const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
 
-      if (logger.shouldLogTrace()) {
-        logger.trace(
-          `instructed amount: ${Amounts.stringify(req.contractTermsAmount)}`,
+      if (!materialAvSel) {
+        const prospectiveAvSel = await internalSelectPayCoins(
+          wex,
+          tx,
+          req,
+          true,
         );
-        logger.trace(`wire fees per exchange: ${j2s(wireFeesPerExchange)}`);
-        logger.trace(`candidates: ${j2s(candidateDenoms)}`);
-      }
 
-      const coinRes: SelectedCoin[] = [];
-      const currency = contractTermsAmount.currency;
-
-      let tally: CoinSelectionTally = {
-        amountPayRemaining: contractTermsAmount,
-        amountDepositFeeLimitRemaining: depositFeeLimit,
-        customerDepositFees: Amounts.zeroOfCurrency(currency),
-        customerWireFees: Amounts.zeroOfCurrency(currency),
-        wireFeeCoveredForExchange: new Set(),
-        lastDepositFee: Amounts.zeroOfCurrency(currency),
-      };
-
-      await maybeRepairCoinSelection(
-        wex,
-        tx,
-        req.prevPayCoins ?? [],
-        coinRes,
-        tally,
-        {
-          wireFeeAmortization: req.wireFeeAmortization,
-          wireFeesPerExchange: wireFeesPerExchange,
-        },
-      );
-
-      let selectedDenom: SelResult | undefined;
-      if (req.forcedSelection) {
-        selectedDenom = selectForced(req, candidateDenoms);
-      } else {
-        // FIXME:  Here, we should select coins in a smarter way.
-        // Instead of always spending the next-largest coin,
-        // we should try to find the smallest coin that covers the
-        // amount.
-        selectedDenom = selectGreedy(
-          {
-            wireFeeAmortization: req.wireFeeAmortization,
-            wireFeesPerExchange: wireFeesPerExchange,
-          },
-          candidateDenoms,
-          tally,
-        );
-      }
+        if (prospectiveAvSel) {
+          const prospectiveCoins: SelectedProspectiveCoin[] = [];
+          for (const avKey of Object.keys(prospectiveAvSel.sel)) {
+            const mySel = prospectiveAvSel.sel[avKey];
+            for (const contrib of mySel.contributions) {
+              prospectiveCoins.push({
+                denomPubHash: mySel.denomPubHash,
+                contribution: Amounts.stringify(contrib),
+                exchangeBaseUrl: mySel.exchangeBaseUrl,
+              });
+            }
+          }
+          return {
+            type: "prospective",
+            result: {
+              prospectiveCoins,
+              customerDepositFees: Amounts.stringify(
+                prospectiveAvSel.tally.customerDepositFees,
+              ),
+              customerWireFees: Amounts.stringify(
+                prospectiveAvSel.tally.customerWireFees,
+              ),
+            },
+          } satisfies SelectPayCoinsResult;
+        }
 
-      if (!selectedDenom) {
         return {
           type: "failure",
           insufficientBalanceDetails: await reportInsufficientBalanceDetails(
@@ -268,9 +334,9 @@ export async function selectPayCoins(
 
       const coinSel = await assembleSelectPayCoinsSuccessResult(
         tx,
-        selectedDenom,
-        coinRes,
-        tally,
+        materialAvSel.sel,
+        materialAvSel.coinRes,
+        materialAvSel.tally,
       );
 
       if (logger.shouldLogTrace()) {
@@ -324,12 +390,18 @@ async function maybeRepairCoinSelection(
     ).amount;
 
     coinRes.push({
+      exchangeBaseUrl: coin.exchangeBaseUrl,
+      denomPubHash: coin.denomPubHash,
       coinPub: prev.coinPub,
       contribution: Amounts.stringify(prev.contribution),
     });
   }
 }
 
+/**
+ * Returns undefined if the success response could not be assembled,
+ * as not enough coins are actually available.
+ */
 async function assembleSelectPayCoinsSuccessResult(
   tx: WalletDbReadOnlyTransaction<["coins"]>,
   finalSel: SelResult,
@@ -359,8 +431,10 @@ async function assembleSelectPayCoinsSuccessResult(
 
     for (let i = 0; i < selInfo.contributions.length; i++) {
       coinRes.push({
+        denomPubHash: coins[i].denomPubHash,
         coinPub: coins[i].coinPub,
         contribution: Amounts.stringify(selInfo.contributions[i]),
+        exchangeBaseUrl: coins[i].exchangeBaseUrl,
       });
     }
   }
@@ -745,6 +819,13 @@ interface SelectPayCandidatesRequest {
   depositPaytoUri?: string;
   restrictExchanges: ExchangeRestrictionSpec | undefined;
   requiredMinimumAge?: number;
+
+  /**
+   * If set to true, the coin selection will also use coins that are not
+   * materially available yet, but that are expected to become available
+   * as the output of a refresh operation.
+   */
+  includePendingCoins: boolean;
 }
 
 async function selectPayCandidates(
@@ -845,9 +926,13 @@ async function selectPayCandidates(
         continue;
       }
       numUsable++;
+      let numAvailable = coinAvail.freshCoinCount ?? 0;
+      if (req.includePendingCoins) {
+        numAvailable += coinAvail.pendingRefreshOutputCount ?? 0;
+      }
       denoms.push({
         ...DenominationRecord.toDenomInfo(denom),
-        numAvailable: coinAvail.freshCoinCount ?? 0,
+        numAvailable,
         maxAge: coinAvail.maxAge,
       });
     }
@@ -886,8 +971,23 @@ export interface PeerCoinSelectionDetails {
   maxExpirationDate: TalerProtocolTimestamp;
 }
 
+export interface ProspectivePeerCoinSelectionDetails {
+  exchangeBaseUrl: string;
+
+  prospectiveCoins: SelectedProspectiveCoin[];
+
+  /**
+   * How much of the deposit fees is the customer paying?
+   */
+  depositFees: AmountJson;
+
+  maxExpirationDate: TalerProtocolTimestamp;
+}
+
 export type SelectPeerCoinsResult =
   | { type: "success"; result: PeerCoinSelectionDetails }
+  // Successful, but using coins that are not materially available yet.
+  | { type: "prospective"; result: ProspectivePeerCoinSelectionDetails }
   | {
       type: "failure";
       insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
@@ -901,6 +1001,13 @@ export interface PeerCoinSelectionRequest {
    * selection instead of selecting completely new coins.
    */
   repair?: PreviousPayCoins;
+
+  /**
+   * If set to true, the coin selection will also use coins that are not
+   * materially available yet, but that are expected to become available
+   * as the output of a refresh operation.
+   */
+  includePendingCoins: boolean;
 }
 
 export async function computeCoinSelMaxExpirationDate(
@@ -968,6 +1075,77 @@ function getGlobalFees(
   return undefined;
 }
 
+async function internalSelectPeerCoins(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<
+    [
+      "exchanges",
+      "contractTerms",
+      "coins",
+      "coinAvailability",
+      "denominations",
+      "refreshGroups",
+      "exchangeDetails",
+    ]
+  >,
+  req: PeerCoinSelectionRequest,
+  exch: ExchangeWireDetails,
+  includePendingCoins: boolean,
+): Promise<
+  | { sel: SelResult; tally: CoinSelectionTally; resCoins: SelectedCoin[] }
+  | undefined
+> {
+  const candidatesRes = await selectPayCandidates(wex, tx, {
+    instructedAmount: req.instructedAmount,
+    restrictExchanges: {
+      auditors: [],
+      exchanges: [
+        {
+          exchangeBaseUrl: exch.exchangeBaseUrl,
+          exchangePub: exch.masterPublicKey,
+        },
+      ],
+    },
+    restrictWireMethod: undefined,
+    includePendingCoins,
+  });
+  const candidates = candidatesRes[0];
+  if (logger.shouldLogTrace()) {
+    logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
+  }
+  const tally = emptyTallyForPeerPayment(req.instructedAmount);
+  const resCoins: SelectedCoin[] = [];
+
+  await maybeRepairCoinSelection(wex, tx, req.repair ?? [], resCoins, tally, {
+    wireFeeAmortization: 1,
+    wireFeesPerExchange: {},
+  });
+
+  if (logger.shouldLogTrace()) {
+    logger.trace(`candidates: ${j2s(candidates)}`);
+    logger.trace(`instructedAmount: ${j2s(req.instructedAmount)}`);
+    logger.trace(`tally: ${j2s(tally)}`);
+  }
+
+  const selRes = selectGreedy(
+    {
+      wireFeeAmortization: 1,
+      wireFeesPerExchange: {},
+    },
+    candidates,
+    tally,
+  );
+  if (!selRes) {
+    return undefined;
+  }
+
+  return {
+    sel: selRes,
+    tally,
+    resCoins,
+  };
+}
+
 export async function selectPeerCoins(
   wex: WalletExecutionContext,
   req: PeerCoinSelectionRequest,
@@ -1004,65 +1182,63 @@ export async function selectPeerCoins(
         if (!globalFees) {
           continue;
         }
-        const candidatesRes = await selectPayCandidates(wex, tx, {
-          instructedAmount,
-          restrictExchanges: {
-            auditors: [],
-            exchanges: [
-              {
-                exchangeBaseUrl: exch.baseUrl,
-                exchangePub: exch.detailsPointer.masterPublicKey,
-              },
-            ],
-          },
-          restrictWireMethod: undefined,
-        });
-        const candidates = candidatesRes[0];
-        if (logger.shouldLogTrace()) {
-          logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
-        }
-        const tally = emptyTallyForPeerPayment(req.instructedAmount);
-        const resCoins: SelectedCoin[] = [];
 
-        await maybeRepairCoinSelection(
+        const avRes = await internalSelectPeerCoins(
           wex,
           tx,
-          req.repair ?? [],
-          resCoins,
-          tally,
-          {
-            wireFeeAmortization: 1,
-            wireFeesPerExchange: {},
-          },
+          req,
+          exchWire,
+          false,
         );
 
-        if (logger.shouldLogTrace()) {
-          logger.trace(`candidates: ${j2s(candidates)}`);
-          logger.trace(`instructedAmount: ${j2s(instructedAmount)}`);
-          logger.trace(`tally: ${j2s(tally)}`);
-        }
-
-        const selectedDenom = selectGreedy(
-          {
-            wireFeeAmortization: 1,
-            wireFeesPerExchange: {},
-          },
-          candidates,
-          tally,
-        );
-
-        if (selectedDenom) {
+        if (!avRes && req.includePendingCoins) {
+          // Try to see if we can do a prospective selection
+          const prospectiveAvRes = await internalSelectPeerCoins(
+            wex,
+            tx,
+            req,
+            exchWire,
+            true,
+          );
+          if (prospectiveAvRes) {
+            const prospectiveCoins: SelectedProspectiveCoin[] = [];
+            for (const avKey of Object.keys(prospectiveAvRes.sel)) {
+              const mySel = prospectiveAvRes.sel[avKey];
+              for (const contrib of mySel.contributions) {
+                prospectiveCoins.push({
+                  denomPubHash: mySel.denomPubHash,
+                  contribution: Amounts.stringify(contrib),
+                  exchangeBaseUrl: mySel.exchangeBaseUrl,
+                });
+              }
+            }
+            const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+              wex,
+              tx,
+              prospectiveAvRes.sel,
+            );
+            return {
+              type: "prospective",
+              result: {
+                prospectiveCoins,
+                depositFees: prospectiveAvRes.tally.customerDepositFees,
+                exchangeBaseUrl: exch.baseUrl,
+                maxExpirationDate,
+              },
+            };
+          }
+        } else if (avRes) {
           const r = await assembleSelectPayCoinsSuccessResult(
             tx,
-            selectedDenom,
-            resCoins,
-            tally,
+            avRes.sel,
+            avRes.resCoins,
+            avRes.tally,
           );
 
           const maxExpirationDate = await computeCoinSelMaxExpirationDate(
             wex,
             tx,
-            selectedDenom,
+            avRes.sel,
           );
 
           return {
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index de22d78a8..7b9dfa2a2 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1787,9 +1787,9 @@ export interface DepositGroupRecord {
 
   contractTermsHash: string;
 
-  payCoinSelection: DbCoinSelection;
+  payCoinSelection?: DbCoinSelection;
 
-  payCoinSelectionUid: string;
+  payCoinSelectionUid?: string;
 
   totalPayCost: AmountString;
 
@@ -1804,7 +1804,7 @@ export interface DepositGroupRecord {
 
   operationStatus: DepositOperationStatus;
 
-  statusPerCoin: DepositElementStatus[];
+  statusPerCoin?: DepositElementStatus[];
 
   infoPerExchange?: Record<string, DepositInfoPerExchange>;
 
diff --git a/packages/taler-wallet-core/src/deposits.ts 
b/packages/taler-wallet-core/src/deposits.ts
index a8612744f..5b23d8325 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -39,10 +39,10 @@ import {
   Logger,
   MerchantContractTerms,
   NotificationType,
-  PayCoinSelection,
   PrepareDepositRequest,
   PrepareDepositResponse,
   RefreshReason,
+  SelectedProspectiveCoin,
   TalerError,
   TalerErrorCode,
   TalerPreciseTimestamp,
@@ -413,16 +413,28 @@ async function refundDepositGroup(
   wex: WalletExecutionContext,
   depositGroup: DepositGroupRecord,
 ): Promise<TaskRunResult> {
-  const newTxPerCoin = [...depositGroup.statusPerCoin];
+  const statusPerCoin = depositGroup.statusPerCoin;
+  const payCoinSelection = depositGroup.payCoinSelection;
+  if (!statusPerCoin) {
+    throw Error(
+      "unable to refund deposit group without coin selection (status missing)",
+    );
+  }
+  if (!payCoinSelection) {
+    throw Error(
+      "unable to refund deposit group without coin selection (selection 
missing)",
+    );
+  }
+  const newTxPerCoin = [...statusPerCoin];
   logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`);
-  for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
-    const st = depositGroup.statusPerCoin[i];
+  for (let i = 0; i < statusPerCoin.length; i++) {
+    const st = statusPerCoin[i];
     switch (st) {
       case DepositElementStatus.RefundFailed:
       case DepositElementStatus.RefundSuccess:
         break;
       default: {
-        const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+        const coinPub = payCoinSelection.coinPubs[i];
         const coinExchange = await wex.db.runReadOnlyTx(
           ["coins"],
           async (tx) => {
@@ -431,7 +443,7 @@ async function refundDepositGroup(
             return coinRecord.exchangeBaseUrl;
           },
         );
-        const refundAmount = 
depositGroup.payCoinSelection.coinContributions[i];
+        const refundAmount = payCoinSelection.coinContributions[i];
         // We use a constant refund transaction ID, since there can
         // only be one refund.
         const rtid = 1;
@@ -503,8 +515,8 @@ async function refundDepositGroup(
       const refreshCoins: CoinRefreshRequest[] = [];
       for (let i = 0; i < newTxPerCoin.length; i++) {
         refreshCoins.push({
-          amount: depositGroup.payCoinSelection.coinContributions[i],
-          coinPub: depositGroup.payCoinSelection.coinPubs[i],
+          amount: payCoinSelection.coinContributions[i],
+          coinPub: payCoinSelection.coinPubs[i],
         });
       }
       let refreshRes: CreateRefreshGroupResult | undefined = undefined;
@@ -740,9 +752,21 @@ async function processDepositGroupPendingTrack(
   wex: WalletExecutionContext,
   depositGroup: DepositGroupRecord,
 ): Promise<TaskRunResult> {
+  const statusPerCoin = depositGroup.statusPerCoin;
+  const payCoinSelection = depositGroup.payCoinSelection;
+  if (!statusPerCoin) {
+    throw Error(
+      "unable to refund deposit group without coin selection (status missing)",
+    );
+  }
+  if (!payCoinSelection) {
+    throw Error(
+      "unable to refund deposit group without coin selection (selection 
missing)",
+    );
+  }
   const { depositGroupId } = depositGroup;
-  for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
-    const coinPub = depositGroup.payCoinSelection.coinPubs[i];
+  for (let i = 0; i < statusPerCoin.length; i++) {
+    const coinPub = payCoinSelection.coinPubs[i];
     // FIXME: Make the URL part of the coin selection?
     const exchangeBaseUrl = await wex.db.runReadWriteTx(
       ["coins"],
@@ -761,7 +785,7 @@ async function processDepositGroupPendingTrack(
         }
       | undefined;
 
-    if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+    if (statusPerCoin[i] !== DepositElementStatus.Wired) {
       const track = await trackDeposit(
         wex,
         depositGroup,
@@ -826,6 +850,9 @@ async function processDepositGroupPendingTrack(
         if (!dg) {
           return;
         }
+        if (!dg.statusPerCoin) {
+          return;
+        }
         if (updatedTxStatus !== undefined) {
           dg.statusPerCoin[i] = updatedTxStatus;
         }
@@ -858,9 +885,12 @@ async function processDepositGroupPendingTrack(
       if (!dg) {
         return undefined;
       }
+      if (!dg.statusPerCoin) {
+        return undefined;
+      }
       const oldTxState = computeDepositTransactionStatus(dg);
-      for (let i = 0; i < depositGroup.statusPerCoin.length; i++) {
-        if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) {
+      for (let i = 0; i < dg.statusPerCoin.length; i++) {
+        if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) {
           allWired = false;
           break;
         }
@@ -924,6 +954,87 @@ async function processDepositGroupPendingDeposit(
   // Check for cancellation before expensive operations.
   cancellationToken?.throwIfCancelled();
 
+  if (!depositGroup.payCoinSelection) {
+    logger.info("missing coin selection for deposit group, selecting now");
+    // FIXME: Consider doing the coin selection inside the txn
+    const payCoinSel = await selectPayCoins(wex, {
+      restrictExchanges: {
+        auditors: [],
+        exchanges: contractData.allowedExchanges,
+      },
+      restrictWireMethod: contractData.wireMethod,
+      contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+      depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
+      wireFeeAmortization: 1, // FIXME #8653
+      prevPayCoins: [],
+    });
+
+    switch (payCoinSel.type) {
+      case "success":
+        logger.info("coin selection success");
+        break;
+      case "failure":
+        logger.info("coin selection failure");
+        throw TalerError.fromDetail(
+          TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+          {
+            insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+          },
+        );
+      case "prospective":
+        logger.info("coin selection prospective");
+        throw Error("insufficient balance (waiting on pending refresh)");
+      default:
+        assertUnreachable(payCoinSel);
+    }
+
+    const transitionDone = await wex.db.runReadWriteTx(
+      [
+        "depositGroups",
+        "coins",
+        "coinAvailability",
+        "refreshGroups",
+        "refreshSessions",
+        "denominations",
+      ],
+      async (tx) => {
+        const dg = await tx.depositGroups.get(depositGroupId);
+        if (!dg) {
+          return false;
+        }
+        if (dg.statusPerCoin) {
+          return false;
+        }
+        dg.payCoinSelection = {
+          coinContributions: payCoinSel.coinSel.coins.map(
+            (x) => x.contribution,
+          ),
+          coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+        };
+        dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+        dg.statusPerCoin = payCoinSel.coinSel.coins.map(
+          () => DepositElementStatus.DepositPending,
+        );
+        await tx.depositGroups.put(dg);
+        await spendCoins(wex, tx, {
+          allocationId: transactionId,
+          coinPubs: dg.payCoinSelection.coinPubs,
+          contributions: dg.payCoinSelection.coinContributions.map((x) =>
+            Amounts.parseOrThrow(x),
+          ),
+          refreshReason: RefreshReason.PayDeposit,
+        });
+        return true;
+      },
+    );
+
+    if (transitionDone) {
+      return TaskRunResult.progress();
+    } else {
+      return TaskRunResult.backoff();
+    }
+  }
+
   // FIXME: Cache these!
   const depositPermissions = await generateDepositPermissions(
     wex,
@@ -990,6 +1101,9 @@ async function processDepositGroupPendingDeposit(
       if (!dg) {
         return;
       }
+      if (!dg.statusPerCoin) {
+        return;
+      }
       for (const batchIndex of batchIndexes) {
         const coinStatus = dg.statusPerCoin[batchIndex];
         switch (coinStatus) {
@@ -1155,11 +1269,8 @@ async function trackDeposit(
 /**
  * Check if creating a deposit group is possible and calculate
  * the associated fees.
- *
- * FIXME: This should be renamed to checkDepositGroup,
- * as it doesn't prepare anything
  */
-export async function prepareDepositGroup(
+export async function checkDepositGroup(
   wex: WalletExecutionContext,
   req: PrepareDepositRequest,
 ): Promise<PrepareDepositResponse> {
@@ -1168,6 +1279,7 @@ export async function prepareDepositGroup(
     throw Error("invalid payto URI");
   }
   const amount = Amounts.parseOrThrow(req.amount);
+  const currency = Amounts.currencyOf(amount);
 
   const exchangeInfos: ExchangeHandle[] = [];
 
@@ -1231,28 +1343,39 @@ export async function prepareDepositGroup(
     prevPayCoins: [],
   });
 
-  if (payCoinSel.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
-      },
-    );
+  let selCoins: SelectedProspectiveCoin[] | undefined = undefined;
+
+  switch (payCoinSel.type) {
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      selCoins = payCoinSel.result.prospectiveCoins;
+      break;
+    case "success":
+      selCoins = payCoinSel.coinSel.coins;
+      break;
+    default:
+      assertUnreachable(payCoinSel);
   }
 
-  const totalDepositCost = await getTotalPaymentCost(wex, payCoinSel.coinSel);
+  const totalDepositCost = await getTotalPaymentCost(wex, currency, selCoins);
 
   const effectiveDepositAmount = await getCounterpartyEffectiveDepositAmount(
     wex,
     p.targetType,
-    payCoinSel.coinSel,
+    selCoins,
   );
 
   const fees = await getTotalFeesForDepositAmount(
     wex,
     p.targetType,
     amount,
-    payCoinSel.coinSel,
+    selCoins,
   );
 
   return {
@@ -1280,6 +1403,7 @@ export async function createDepositGroup(
   }
 
   const amount = Amounts.parseOrThrow(req.amount);
+  const currency = amount.currency;
 
   const exchangeInfos: { url: string; master_pub: string }[] = [];
 
@@ -1350,16 +1474,27 @@ export async function createDepositGroup(
     prevPayCoins: [],
   });
 
-  if (payCoinSel.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
-      },
-    );
+  let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+  switch (payCoinSel.type) {
+    case "success":
+      coins = payCoinSel.coinSel.coins;
+      break;
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      coins = payCoinSel.result.prospectiveCoins;
+      break;
+    default:
+      assertUnreachable(payCoinSel);
   }
 
-  const totalDepositCost = await getTotalPaymentCost(wex, payCoinSel.coinSel);
+  const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
 
   let depositGroupId: string;
   if (req.transactionId) {
@@ -1374,34 +1509,23 @@ export async function createDepositGroup(
 
   const infoPerExchange: Record<string, DepositInfoPerExchange> = {};
 
-  await wex.db.runReadOnlyTx(["coins"], async (tx) => {
-    for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) {
-      const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub);
-      if (!coin) {
-        logger.error("coin not found anymore");
-        continue;
-      }
-      let depPerExchange = infoPerExchange[coin.exchangeBaseUrl];
-      if (!depPerExchange) {
-        infoPerExchange[coin.exchangeBaseUrl] = depPerExchange = {
-          amountEffective: Amounts.stringify(
-            Amounts.zeroOfAmount(totalDepositCost),
-          ),
-        };
-      }
-      const contrib = payCoinSel.coinSel.coins[i].contribution;
-      depPerExchange.amountEffective = Amounts.stringify(
-        Amounts.add(depPerExchange.amountEffective, contrib).amount,
-      );
+  for (let i = 0; i < coins.length; i++) {
+    let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl];
+    if (!depPerExchange) {
+      infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = {
+        amountEffective: Amounts.stringify(
+          Amounts.zeroOfAmount(totalDepositCost),
+        ),
+      };
     }
-  });
+    const contrib = coins[i].contribution;
+    depPerExchange.amountEffective = Amounts.stringify(
+      Amounts.add(depPerExchange.amountEffective, contrib).amount,
+    );
+  }
 
   const counterpartyEffectiveDepositAmount =
-    await getCounterpartyEffectiveDepositAmount(
-      wex,
-      p.targetType,
-      payCoinSel.coinSel,
-    );
+    await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins);
 
   const depositGroup: DepositGroupRecord = {
     contractTermsHash,
@@ -1414,14 +1538,9 @@ export async function createDepositGroup(
       AbsoluteTime.toPreciseTimestamp(now),
     ),
     timestampFinished: undefined,
-    statusPerCoin: payCoinSel.coinSel.coins.map(
-      () => DepositElementStatus.DepositPending,
-    ),
-    payCoinSelection: {
-      coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
-      coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
-    },
-    payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
+    statusPerCoin: undefined,
+    payCoinSelection: undefined,
+    payCoinSelectionUid: undefined,
     merchantPriv: merchantPair.priv,
     merchantPub: merchantPair.pub,
     totalPayCost: Amounts.stringify(totalDepositCost),
@@ -1439,6 +1558,17 @@ export async function createDepositGroup(
     infoPerExchange,
   };
 
+  if (payCoinSel.type === "success") {
+    depositGroup.payCoinSelection = {
+      coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
+      coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+    };
+    depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
+    depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map(
+      () => DepositElementStatus.DepositPending,
+    );
+  }
+
   const ctx = new DepositTransactionContext(wex, depositGroupId);
   const transactionId = ctx.transactionId;
 
@@ -1454,14 +1584,16 @@ export async function createDepositGroup(
       "contractTerms",
     ],
     async (tx) => {
-      await spendCoins(wex, tx, {
-        allocationId: transactionId,
-        coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
-        contributions: payCoinSel.coinSel.coins.map((x) =>
-          Amounts.parseOrThrow(x.contribution),
-        ),
-        refreshReason: RefreshReason.PayDeposit,
-      });
+      if (depositGroup.payCoinSelection) {
+        await spendCoins(wex, tx, {
+          allocationId: transactionId,
+          coinPubs: depositGroup.payCoinSelection.coinPubs,
+          contributions: depositGroup.payCoinSelection.coinContributions.map(
+            (x) => Amounts.parseOrThrow(x),
+          ),
+          refreshReason: RefreshReason.PayDeposit,
+        });
+      }
       await tx.depositGroups.put(depositGroup);
       await tx.contractTerms.put({
         contractTermsRaw: contractTerms,
@@ -1500,7 +1632,7 @@ export async function createDepositGroup(
 export async function getCounterpartyEffectiveDepositAmount(
   wex: WalletExecutionContext,
   wireType: string,
-  pcs: PayCoinSelection,
+  pcs: SelectedProspectiveCoin[],
 ): Promise<AmountJson> {
   const amt: AmountJson[] = [];
   const fees: AmountJson[] = [];
@@ -1509,23 +1641,19 @@ export async function 
getCounterpartyEffectiveDepositAmount(
   await wex.db.runReadOnlyTx(
     ["coins", "denominations", "exchangeDetails", "exchanges"],
     async (tx) => {
-      for (let i = 0; i < pcs.coins.length; i++) {
-        const coin = await tx.coins.get(pcs.coins[i].coinPub);
-        if (!coin) {
-          throw Error("can't calculate deposit amount, coin not found");
-        }
+      for (let i = 0; i < pcs.length; i++) {
         const denom = await getDenomInfo(
           wex,
           tx,
-          coin.exchangeBaseUrl,
-          coin.denomPubHash,
+          pcs[i].exchangeBaseUrl,
+          pcs[i].denomPubHash,
         );
         if (!denom) {
           throw Error("can't find denomination to calculate deposit amount");
         }
-        amt.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
+        amt.push(Amounts.parseOrThrow(pcs[i].contribution));
         fees.push(Amounts.parseOrThrow(denom.feeDeposit));
-        exchangeSet.add(coin.exchangeBaseUrl);
+        exchangeSet.add(pcs[i].exchangeBaseUrl);
       }
 
       for (const exchangeUrl of exchangeSet.values()) {
@@ -1564,7 +1692,7 @@ async function getTotalFeesForDepositAmount(
   wex: WalletExecutionContext,
   wireType: string,
   total: AmountJson,
-  pcs: PayCoinSelection,
+  pcs: SelectedProspectiveCoin[],
 ): Promise<DepositGroupFees> {
   const wireFee: AmountJson[] = [];
   const coinFee: AmountJson[] = [];
@@ -1575,33 +1703,26 @@ async function getTotalFeesForDepositAmount(
   await wex.db.runReadOnlyTx(
     ["coins", "denominations", "exchanges", "exchangeDetails"],
     async (tx) => {
-      for (let i = 0; i < pcs.coins.length; i++) {
-        const coin = await tx.coins.get(pcs.coins[i].coinPub);
-        if (!coin) {
-          throw Error("can't calculate deposit amount, coin not found");
-        }
+      for (let i = 0; i < pcs.length; i++) {
         const denom = await getDenomInfo(
           wex,
           tx,
-          coin.exchangeBaseUrl,
-          coin.denomPubHash,
+          pcs[i].exchangeBaseUrl,
+          pcs[i].denomPubHash,
         );
         if (!denom) {
           throw Error("can't find denomination to calculate deposit amount");
         }
         coinFee.push(Amounts.parseOrThrow(denom.feeDeposit));
-        exchangeSet.add(coin.exchangeBaseUrl);
+        exchangeSet.add(pcs[i].exchangeBaseUrl);
 
         const allDenoms = await getCandidateWithdrawalDenomsTx(
           wex,
           tx,
-          coin.exchangeBaseUrl,
+          pcs[i].exchangeBaseUrl,
           currency,
         );
-        const amountLeft = Amounts.sub(
-          denom.value,
-          pcs.coins[i].contribution,
-        ).amount;
+        const amountLeft = Amounts.sub(denom.value, 
pcs[i].contribution).amount;
         const refreshCost = getTotalRefreshCost(
           allDenoms,
           denom,
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts 
b/packages/taler-wallet-core/src/dev-experiments.ts
index 57810dbf4..7cf18e36c 100644
--- a/packages/taler-wallet-core/src/dev-experiments.ts
+++ b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -67,52 +67,56 @@ export async function applyDevExperiment(
     throw Error("can't handle devmode URI unless devmode is active");
   }
 
-  if (parsedUri.devExperimentId === "start-block-refresh") {
-    wex.ws.devExperimentState.blockRefreshes = true;
-    return;
-  }
-
-  if (parsedUri.devExperimentId == "insert-pending-refresh") {
-    await wex.db.runReadWriteTx(["refreshGroups"], async (tx) => {
-      const refreshGroupId = encodeCrock(getRandomBytes(32));
-      const newRg: RefreshGroupRecord = {
-        currency: "TESTKUDOS",
-        expectedOutputPerCoin: [],
-        inputPerCoin: [],
-        oldCoinPubs: [],
-        operationStatus: RefreshOperationStatus.Pending,
-        reason: RefreshReason.Manual,
-        refreshGroupId,
-        statusPerCoin: [],
-        timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
-        timestampFinished: undefined,
-        originatingTransactionId: undefined,
-        infoPerExchange: {},
-      };
-      await tx.refreshGroups.put(newRg);
-    });
-    return;
-  }
-
-  if (parsedUri.devExperimentId == "insert-denom-loss") {
-    await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => {
-      const eventId = encodeCrock(getRandomBytes(32));
-      const newRg: DenomLossEventRecord = {
-        amount: "TESTKUDOS:42",
-        currency: "TESTKUDOS",
-        exchangeBaseUrl: "https://exchange.test.taler.net/";,
-        denomLossEventId: eventId,
-        denomPubHashes: [
-          encodeCrock(getRandomBytes(64)),
-          encodeCrock(getRandomBytes(64)),
-        ],
-        eventType: DenomLossEventType.DenomExpired,
-        status: DenomLossStatus.Done,
-        timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
-      };
-      await tx.denomLossEvents.put(newRg);
-    });
-    return;
+  switch (parsedUri.devExperimentId) {
+    case "start-block-refresh": {
+      wex.ws.devExperimentState.blockRefreshes = true;
+      return;
+    }
+    case "stop-block-refresh": {
+      wex.ws.devExperimentState.blockRefreshes = false;
+      return;
+    }
+    case "insert-pending-refresh": {
+      await wex.db.runReadWriteTx(["refreshGroups"], async (tx) => {
+        const refreshGroupId = encodeCrock(getRandomBytes(32));
+        const newRg: RefreshGroupRecord = {
+          currency: "TESTKUDOS",
+          expectedOutputPerCoin: [],
+          inputPerCoin: [],
+          oldCoinPubs: [],
+          operationStatus: RefreshOperationStatus.Pending,
+          reason: RefreshReason.Manual,
+          refreshGroupId,
+          statusPerCoin: [],
+          timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+          timestampFinished: undefined,
+          originatingTransactionId: undefined,
+          infoPerExchange: {},
+        };
+        await tx.refreshGroups.put(newRg);
+      });
+      return;
+    }
+    case "insert-denom-loss": {
+      await wex.db.runReadWriteTx(["denomLossEvents"], async (tx) => {
+        const eventId = encodeCrock(getRandomBytes(32));
+        const newRg: DenomLossEventRecord = {
+          amount: "TESTKUDOS:42",
+          currency: "TESTKUDOS",
+          exchangeBaseUrl: "https://exchange.test.taler.net/";,
+          denomLossEventId: eventId,
+          denomPubHashes: [
+            encodeCrock(getRandomBytes(64)),
+            encodeCrock(getRandomBytes(64)),
+          ],
+          eventType: DenomLossEventType.DenomExpired,
+          status: DenomLossStatus.Done,
+          timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+        };
+        await tx.denomLossEvents.put(newRg);
+      });
+      return;
+    }
   }
 
   throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts 
b/packages/taler-wallet-core/src/pay-merchant.ts
index 3b58c1e0a..25725052c 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -63,12 +63,12 @@ import {
   parsePayTemplateUri,
   parsePayUri,
   parseTalerUri,
-  PayCoinSelection,
   PreparePayResult,
   PreparePayResultType,
   PreparePayTemplateRequest,
   randomBytes,
   RefreshReason,
+  SelectedProspectiveCoin,
   SharePaymentResult,
   StartRefundQueryForUriResponse,
   stringifyPayUri,
@@ -453,19 +453,15 @@ export class RefundTransactionContext implements 
TransactionContext {
  */
 export async function getTotalPaymentCost(
   wex: WalletExecutionContext,
-  pcs: PayCoinSelection,
+  currency: string,
+  pcs: SelectedProspectiveCoin[],
 ): Promise<AmountJson> {
-  const currency = Amounts.currencyOf(pcs.customerDepositFees);
   return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
     const costs: AmountJson[] = [];
-    for (let i = 0; i < pcs.coins.length; i++) {
-      const coin = await tx.coins.get(pcs.coins[i].coinPub);
-      if (!coin) {
-        throw Error("can't calculate payment cost, coin not found");
-      }
+    for (let i = 0; i < pcs.length; i++) {
       const denom = await tx.denominations.get([
-        coin.exchangeBaseUrl,
-        coin.denomPubHash,
+        pcs[i].exchangeBaseUrl,
+        pcs[i].denomPubHash,
       ]);
       if (!denom) {
         throw Error(
@@ -475,23 +471,20 @@ export async function getTotalPaymentCost(
       const allDenoms = await getCandidateWithdrawalDenomsTx(
         wex,
         tx,
-        coin.exchangeBaseUrl,
+        pcs[i].exchangeBaseUrl,
         currency,
       );
-      const amountLeft = Amounts.sub(
-        denom.value,
-        pcs.coins[i].contribution,
-      ).amount;
+      const amountLeft = Amounts.sub(denom.value, pcs[i].contribution).amount;
       const refreshCost = getTotalRefreshCost(
         allDenoms,
         DenominationRecord.toDenomInfo(denom),
         amountLeft,
         wex.ws.config.testing.denomselAllowLate,
       );
-      costs.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
+      costs.push(Amounts.parseOrThrow(pcs[i].contribution));
       costs.push(refreshCost);
     }
-    const zero = Amounts.zeroOfAmount(pcs.customerDepositFees);
+    const zero = Amounts.zeroOfCurrency(currency);
     return Amounts.sum([zero, ...costs]).amount;
   });
 }
@@ -1256,6 +1249,8 @@ async function checkPaymentByProposalId(
 
   proposalId = proposal.proposalId;
 
+  const currency = Amounts.currencyOf(contractData.amount);
+
   const ctx = new PayMerchantTransactionContext(wex, proposalId);
 
   const transactionId = ctx.transactionId;
@@ -1293,23 +1288,37 @@ async function checkPaymentByProposalId(
       restrictWireMethod: contractData.wireMethod,
     });
 
-    if (res.type !== "success") {
-      logger.info("not allowing payment, insufficient coins");
-      logger.info(
-        `insufficient balance details: ${j2s(res.insufficientBalanceDetails)}`,
-      );
-      return {
-        status: PreparePayResultType.InsufficientBalance,
-        contractTerms: d.contractTermsRaw,
-        proposalId: proposal.proposalId,
-        transactionId,
-        amountRaw: Amounts.stringify(d.contractData.amount),
-        talerUri,
-        balanceDetails: res.insufficientBalanceDetails,
-      };
+    switch (res.type) {
+      case "failure": {
+        logger.info("not allowing payment, insufficient coins");
+        logger.info(
+          `insufficient balance details: ${j2s(
+            res.insufficientBalanceDetails,
+          )}`,
+        );
+        return {
+          status: PreparePayResultType.InsufficientBalance,
+          contractTerms: d.contractTermsRaw,
+          proposalId: proposal.proposalId,
+          transactionId,
+          amountRaw: Amounts.stringify(d.contractData.amount),
+          talerUri,
+          balanceDetails: res.insufficientBalanceDetails,
+        };
+      }
+      case "prospective":
+        throw Error("insufficient balance (waiting on refresh)");
+      case "success":
+        break;
+      default:
+        assertUnreachable(res);
     }
 
-    const totalCost = await getTotalPaymentCost(wex, res.coinSel);
+    const totalCost = await getTotalPaymentCost(
+      wex,
+      currency,
+      res.coinSel.coins,
+    );
     logger.trace("costInfo", totalCost);
     logger.trace("coinsForPayment", res);
 
@@ -1813,6 +1822,8 @@ export async function confirmPay(
 
   const contractData = d.contractData;
 
+  const currency = Amounts.currencyOf(contractData.amount);
+
   const selectCoinsResult = await selectPayCoins(wex, {
     restrictExchanges: {
       auditors: [],
@@ -1827,18 +1838,31 @@ export async function confirmPay(
     forcedSelection: forcedCoinSel,
   });
 
-  logger.trace("coin selection result", selectCoinsResult);
-
-  if (selectCoinsResult.type === "failure") {
-    // 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.
-    logger.warn("not confirming payment, insufficient coins");
-    throw Error("insufficient balance");
+  switch (selectCoinsResult.type) {
+    case "failure": {
+      // 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.
+      logger.warn("not confirming payment, insufficient coins");
+      throw Error("insufficient balance");
+    }
+    case "prospective": {
+      throw Error("insufficient balance (waiting on refresh)");
+    }
+    case "success":
+      break;
+    default:
+      assertUnreachable(selectCoinsResult);
   }
 
+  logger.trace("coin selection result", selectCoinsResult);
+
   const coinSelection = selectCoinsResult.coinSel;
-  const payCostInfo = await getTotalPaymentCost(wex, coinSelection);
+  const payCostInfo = await getTotalPaymentCost(
+    wex,
+    currency,
+    coinSelection.coins,
+  );
 
   let sessionId: string | undefined;
   if (sessionIdOverride) {
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
index da68d7839..2cc241187 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -370,13 +370,26 @@ async function handlePurseCreationConflict(
     }
   }
 
-  const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
+  const coinSelRes = await selectPeerCoins(ws, {
+    instructedAmount,
+    repair,
+    includePendingCoins: false,
+  });
 
-  if (coinSelRes.type == "failure") {
-    // FIXME: Details!
-    throw Error(
-      "insufficient balance to re-select coins to repair double spending",
-    );
+  switch (coinSelRes.type) {
+    case "failure":
+      // FIXME: Details!
+      throw Error(
+        "insufficient balance to re-select coins to repair double spending",
+      );
+    case "prospective":
+      throw Error(
+        "insufficient balance to re-select coins to repair double spending 
(blocked on refresh)",
+      );
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
 
   const totalAmount = await getTotalPeerPaymentCost(
@@ -583,18 +596,30 @@ export async function confirmPeerPullDebit(
 
   const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
 
-  const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+  // FIXME: Select coins once with pending coins, once without.
+
+  const coinSelRes = await selectPeerCoins(wex, {
+    instructedAmount,
+    includePendingCoins: false,
+  });
   if (logger.shouldLogTrace()) {
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
   }
 
-  if (coinSelRes.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
-      },
-    );
+  switch (coinSelRes.type) {
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      throw Error("insufficient balance (blocked on refresh)");
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
 
   const sel = coinSelRes.result;
@@ -758,18 +783,28 @@ export async function preparePeerPullDebit(
 
   const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
 
-  const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+  const coinSelRes = await selectPeerCoins(wex, {
+    instructedAmount,
+    includePendingCoins: true,
+  });
   if (logger.shouldLogTrace()) {
     logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
   }
 
-  if (coinSelRes.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
-      },
-    );
+  switch (coinSelRes.type) {
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      throw Error("insufficient balance (waiting on refresh)");
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
 
   const totalAmount = await getTotalPeerPaymentCost(
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts 
b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
index 20001e040..51b865b99 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -343,14 +343,24 @@ export async function checkPeerPushDebit(
   logger.trace(
     `checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
   );
-  const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
-  if (coinSelRes.type === "failure") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
-      },
-    );
+  const coinSelRes = await selectPeerCoins(wex, {
+    instructedAmount,
+    includePendingCoins: true,
+  });
+  switch (coinSelRes.type) {
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      throw Error("not supported");
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
   logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`);
   const totalAmount = await getTotalPeerPaymentCost(
@@ -402,13 +412,23 @@ async function handlePurseCreationConflict(
     }
   }
 
-  const coinSelRes = await selectPeerCoins(wex, { instructedAmount, repair });
+  const coinSelRes = await selectPeerCoins(wex, {
+    instructedAmount,
+    repair,
+    includePendingCoins: false,
+  });
 
-  if (coinSelRes.type == "failure") {
-    // FIXME: Details!
-    throw Error(
-      "insufficient balance to re-select coins to repair double spending",
-    );
+  switch (coinSelRes.type) {
+    case "failure":
+    case "prospective":
+      // FIXME: Details!
+      throw Error(
+        "insufficient balance to re-select coins to repair double spending",
+      );
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
 
   await wex.db.runReadWriteTx(["peerPushDebit"], async (tx) => {
@@ -934,15 +954,26 @@ export async function initiatePeerPushDebit(
 
   const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});
 
-  const coinSelRes = await selectPeerCoins(wex, { instructedAmount });
+  // FIXME: Check first if possible with pending coins, in that case defer 
coin selection
+  const coinSelRes = await selectPeerCoins(wex, {
+    instructedAmount,
+    includePendingCoins: false,
+  });
 
-  if (coinSelRes.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
-      },
-    );
+  switch (coinSelRes.type) {
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      throw Error("blocked on pending refresh");
+    case "success":
+      break;
+    default:
+      assertUnreachable(coinSelRes);
   }
 
   const sel = coinSelRes.result;
diff --git a/packages/taler-wallet-core/src/transactions.ts 
b/packages/taler-wallet-core/src/transactions.ts
index e404c0354..463aa97ba 100644
--- a/packages/taler-wallet-core/src/transactions.ts
+++ b/packages/taler-wallet-core/src/transactions.ts
@@ -894,10 +894,14 @@ function buildTransactionForDeposit(
   ort?: OperationRetryRecord,
 ): Transaction {
   let deposited = true;
-  for (const d of dg.statusPerCoin) {
-    if (d == DepositElementStatus.DepositPending) {
-      deposited = false;
+  if (dg.statusPerCoin) {
+    for (const d of dg.statusPerCoin) {
+      if (d == DepositElementStatus.DepositPending) {
+        deposited = false;
+      }
     }
+  } else {
+    deposited = false;
   }
 
   const trackingState: DepositTransactionTrackingState[] = [];
@@ -911,6 +915,17 @@ function buildTransactionForDeposit(
     });
   }
 
+  let wireTransferProgress = 0;
+  if (dg.statusPerCoin) {
+    wireTransferProgress =
+      (100 *
+        dg.statusPerCoin.reduce(
+          (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
+          0,
+        )) /
+      dg.statusPerCoin.length;
+  }
+
   const txState = computeDepositTransactionStatus(dg);
   return {
     type: TransactionType.Deposit,
@@ -927,13 +942,7 @@ function buildTransactionForDeposit(
       tag: TransactionType.Deposit,
       depositGroupId: dg.depositGroupId,
     }),
-    wireTransferProgress:
-      (100 *
-        dg.statusPerCoin.reduce(
-          (prev, cur) => prev + (cur === DepositElementStatus.Wired ? 1 : 0),
-          0,
-        )) /
-      dg.statusPerCoin.length,
+    wireTransferProgress,
     depositGroupId: dg.depositGroupId,
     trackingState,
     deposited,
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index f531c32a3..eb981e79c 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -188,7 +188,7 @@ import {
 import {
   createDepositGroup,
   generateDepositGroupTxId,
-  prepareDepositGroup,
+  checkDepositGroup,
 } from "./deposits.js";
 import { DevExperimentHttpLib, applyDevExperiment } from 
"./dev-experiments.js";
 import {
@@ -1191,7 +1191,7 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
     }
     case WalletApiOperation.PrepareDeposit: {
       const req = codecForPrepareDepositRequest().decode(payload);
-      return await prepareDepositGroup(wex, req);
+      return await checkDepositGroup(wex, req);
     }
     case WalletApiOperation.GenerateDepositGroupTxId:
       return {

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