gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/02: wallet-core: preparations for deferred coin s


From: gnunet
Subject: [taler-wallet-core] 01/02: wallet-core: preparations for deferred coin selection
Date: Wed, 03 Apr 2024 16:21:37 +0200

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

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

commit 5417b8b7b866f1c4f4d99d6ec9ad001af67822b6
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Apr 3 12:58:01 2024 +0200

    wallet-core: preparations for deferred coin selection
---
 .../test-wallet-refresh-blocked.ts                 |  12 +-
 packages/taler-util/src/wallet-types.ts            |  34 +-
 packages/taler-wallet-core/src/coinSelection.ts    | 400 +++++++++++++++------
 packages/taler-wallet-core/src/deposits.ts         | 107 +++---
 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/wallet.ts           |   4 +-
 8 files changed, 562 insertions(+), 255 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts 
b/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts
index 8c568d190..4662c5110 100644
--- a/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts
+++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh-blocked.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { j2s } from "@gnu-taler/taler-util";
+import { AmountString, 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";
@@ -106,7 +106,15 @@ 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: "payto://x-taler-bank/localhost/myuser",
+  });
+
+  console.log(`check resp: ${j2s(depositCheckResp)}`);
+
+  // t.assertTrue(false);
 }
 
 runWalletRefreshBlockedTest.suites = ["wallet"];
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/deposits.ts 
b/packages/taler-wallet-core/src/deposits.ts
index a8612744f..05a5d780a 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,
@@ -1155,11 +1155,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 +1165,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 +1229,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 +1289,7 @@ export async function createDepositGroup(
   }
 
   const amount = Amounts.parseOrThrow(req.amount);
+  const currency = amount.currency;
 
   const exchangeInfos: { url: string; master_pub: string }[] = [];
 
@@ -1350,16 +1360,28 @@ export async function createDepositGroup(
     prevPayCoins: [],
   });
 
-  if (payCoinSel.type !== "success") {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
-      {
-        insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
-      },
-    );
+  switch (payCoinSel.type) {
+    case "success":
+      break;
+    case "failure":
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE,
+        {
+          insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
+        },
+      );
+    case "prospective":
+      // FIXME: Here we need to create the deposit group without a full coin 
selection!
+      throw Error("insufficient balance (pending refresh)");
+    default:
+      assertUnreachable(payCoinSel);
   }
 
-  const totalDepositCost = await getTotalPaymentCost(wex, payCoinSel.coinSel);
+  const totalDepositCost = await getTotalPaymentCost(
+    wex,
+    currency,
+    payCoinSel.coinSel.coins,
+  );
 
   let depositGroupId: string;
   if (req.transactionId) {
@@ -1400,7 +1422,7 @@ export async function createDepositGroup(
     await getCounterpartyEffectiveDepositAmount(
       wex,
       p.targetType,
-      payCoinSel.coinSel,
+      payCoinSel.coinSel.coins,
     );
 
   const depositGroup: DepositGroupRecord = {
@@ -1500,7 +1522,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 +1531,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 +1582,7 @@ async function getTotalFeesForDepositAmount(
   wex: WalletExecutionContext,
   wireType: string,
   total: AmountJson,
-  pcs: PayCoinSelection,
+  pcs: SelectedProspectiveCoin[],
 ): Promise<DepositGroupFees> {
   const wireFee: AmountJson[] = [];
   const coinFee: AmountJson[] = [];
@@ -1575,33 +1593,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/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/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]