gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: wallet-core: implement insuff


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: implement insufficient balance details
Date: Thu, 05 Jan 2023 18:45:57 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 92f1b5928 wallet-core: implement insufficient balance details
92f1b5928 is described below

commit 92f1b5928c764b3af12a29b97bbc3e434a82b1b0
Author: Florian Dold <florian@dold.me>
AuthorDate: Thu Jan 5 18:45:49 2023 +0100

    wallet-core: implement insufficient balance details
    
    For now, only for merchant payments
---
 packages/taler-util/src/wallet-types.ts            |  30 ++-
 packages/taler-wallet-core/src/db.ts               |  15 ++
 .../taler-wallet-core/src/internal-wallet-state.ts |   1 +
 .../src/operations/backup/import.ts                |   1 +
 .../taler-wallet-core/src/operations/balance.ts    | 287 ++++++++++++++++++---
 .../taler-wallet-core/src/operations/common.ts     |   2 +
 .../taler-wallet-core/src/operations/deposits.ts   |  24 +-
 .../src/operations/pay-merchant.ts                 |  99 +++++--
 .../taler-wallet-core/src/operations/recoup.ts     |   1 +
 .../taler-wallet-core/src/operations/refresh.ts    |   5 +-
 .../taler-wallet-core/src/util/coinSelection.ts    |   3 +
 packages/taler-wallet-core/src/wallet.ts           |   4 +
 12 files changed, 392 insertions(+), 80 deletions(-)

diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index d7685fa6e..6b3e39794 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -419,6 +419,7 @@ export const codecForPreparePayResultInsufficientBalance =
         "status",
         codecForConstString(PreparePayResultType.InsufficientBalance),
       )
+      .property("balanceDetails", 
codecForPayMerchantInsufficientBalanceDetails())
       .build("PreparePayResultInsufficientBalance");
 
 export const codecForPreparePayResultAlreadyConfirmed =
@@ -483,6 +484,7 @@ export interface PreparePayResultInsufficientBalance {
   amountRaw: string;
   noncePriv: string;
   talerUri: string;
+  balanceDetails: PayMerchantInsufficientBalanceDetails;
 }
 
 export interface PreparePayResultAlreadyConfirmed {
@@ -2090,32 +2092,32 @@ export interface PayMerchantInsufficientBalanceDetails {
   /**
    * Amount requested by the merchant.
    */
-  amountRequested: AmountJson;
+  amountRequested: AmountString;
 
   /**
    * Balance of type "available" (see balance.ts for definition).
    */
-  balanceAvailable: AmountJson;
+  balanceAvailable: AmountString;
 
   /**
    * Balance of type "material" (see balance.ts for definition).
    */
-  balanceMaterial: AmountJson;
+  balanceMaterial: AmountString;
 
   /**
    * Balance of type "age-acceptable" (see balance.ts for definition).
    */
-  balanceAgeAcceptable: AmountJson;
+  balanceAgeAcceptable: AmountString;
 
   /**
    * Balance of type "merchant-acceptable" (see balance.ts for definition).
    */
-  balanceMechantAcceptable: AmountJson;
+  balanceMerchantAcceptable: AmountString;
 
   /**
    * Balance of type "merchant-depositable" (see balance.ts for definition).
    */
-  balanceMechantDepositable: AmountJson;
+  balanceMerchantDepositable: AmountString;
 
   /**
    * If the payment would succeed without fees
@@ -2126,5 +2128,17 @@ export interface PayMerchantInsufficientBalanceDetails {
    * It is not possible to give an exact value here, since it depends
    * on the coin selection for the amount that would be additionally withdrawn.
    */
-  feeGapEstimate: AmountJson;
-}
\ No newline at end of file
+  feeGapEstimate: AmountString;
+}
+
+const codecForPayMerchantInsufficientBalanceDetails = 
+(): Codec<PayMerchantInsufficientBalanceDetails> =>
+  buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
+    .property("amountRequested", codecForAmountString())
+    .property("balanceAgeAcceptable", codecForAmountString())
+    .property("balanceAvailable", codecForAmountString())
+    .property("balanceMaterial", codecForAmountString())
+    .property("balanceMerchantAcceptable", codecForAmountString())
+    .property("balanceMerchantDepositable", codecForAmountString())
+    .property("feeGapEstimate", codecForAmountString())
+    .build("PayMerchantInsufficientBalanceDetails");
\ No newline at end of file
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 2bf417cac..299c7a36c 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -835,6 +835,14 @@ export enum RefreshOperationStatus {
   FinishedWithError = 51 /* DORMANT_START + 1 */,
 }
 
+/**
+ * Group of refresh operations.  The refreshed coins do not
+ * have to belong to the same exchange, but must have the same
+ * currency.
+ * 
+ * FIXME: Should include the currency as a top-level field, 
+ *        but we need to write a migration for that.
+ */
 export interface RefreshGroupRecord {
   operationStatus: RefreshOperationStatus;
 
@@ -847,6 +855,13 @@ export interface RefreshGroupRecord {
    */
   refreshGroupId: string;
 
+  /**
+   * Currency of this refresh group.
+   * 
+   * FIXME: Write a migration to add this to earlier DB versions.
+   */
+  currency: string;
+
   /**
    * Reason why this refresh group has been created.
    */
diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts 
b/packages/taler-wallet-core/src/internal-wallet-state.ts
index 93d813cc9..879d18a48 100644
--- a/packages/taler-wallet-core/src/internal-wallet-state.ts
+++ b/packages/taler-wallet-core/src/internal-wallet-state.ts
@@ -86,6 +86,7 @@ export interface RefreshOperations {
       refreshGroups: typeof WalletStoresV1.refreshGroups;
       coinAvailability: typeof WalletStoresV1.coinAvailability;
     }>,
+    currency: string,
     oldCoinPubs: CoinRefreshRequest[],
     reason: RefreshReason,
   ): Promise<RefreshGroupId>;
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 5fd220113..805b0c6d3 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -778,6 +778,7 @@ export async function importBackup(
             timestampFinished: backupRefreshGroup.timestamp_finish,
             timestampCreated: backupRefreshGroup.timestamp_created,
             refreshGroupId: backupRefreshGroup.refresh_group_id,
+            currency: 
Amounts.currencyOf(backupRefreshGroup.old_coins[0].input_amount),
             reason,
             lastErrorPerCoin: {},
             oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
diff --git a/packages/taler-wallet-core/src/operations/balance.ts 
b/packages/taler-wallet-core/src/operations/balance.ts
index 95ade1cb4..f697679af 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -16,15 +16,15 @@
 
 /**
  * Functions to compute the wallet's balance.
- * 
+ *
  * There are multiple definition of the wallet's balance.
  * We use the following terminology:
- * 
+ *
  * - "available": Balance that the wallet believes will certainly be available
  *   for spending, modulo any failures of the exchange or double spending 
issues.
  *   This includes available coins *not* allocated to any
  *   spending/refresh/... operation. Pending withdrawals are *not* counted
- *   towards this balance, because they are not certain to succeed. 
+ *   towards this balance, because they are not certain to succeed.
  *   Pending refreshes *are* counted towards this balance.
  *   This balance type is nice to show to the user, because it does not
  *   temporarily decrease after payment when we are waiting for refreshes
@@ -38,12 +38,11 @@
  *
  * - "merchant-acceptable": Subset of the material balance that can be spent 
with a particular
  *   merchant (restricted via min age, exchange, auditor, wire_method).
- * 
+ *
  * - "merchant-depositable": Subset of the merchant-acceptable balance that 
the merchant
  *   can accept via their supported wire methods.
  */
 
-
 /**
  * Imports.
  */
@@ -52,10 +51,16 @@ import {
   BalancesResponse,
   Amounts,
   Logger,
+  AuditorHandle,
+  ExchangeHandle,
+  canonicalizeBaseUrl,
+  parsePaytoUri,
 } from "@gnu-taler/taler-util";
-import { WalletStoresV1 } from "../db.js";
+import { AllowedAuditorInfo, AllowedExchangeInfo, RefreshGroupRecord, 
WalletStoresV1 } from "../db.js";
 import { GetReadOnlyAccess } from "../util/query.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
+import { getExchangeDetails } from "./exchanges.js";
+import { checkLogicInvariant } from "../util/invariants.js";
 
 /**
  * Logger.
@@ -68,6 +73,30 @@ interface WalletBalance {
   pendingOutgoing: AmountJson;
 }
 
+/**
+ * Compute the available amount that the wallet expects to get
+ * out of a refresh group.
+ */
+function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson 
{
+  // Don't count finished refreshes, since the refresh already resulted
+  // in coins being added to the wallet.
+  let available = Amounts.zeroOfCurrency(r.currency);
+  if (r.timestampFinished) {
+    return available;
+  }
+  for (let i = 0; i < r.oldCoinPubs.length; i++) {
+    const session = r.refreshSessionPerCoin[i];
+    if (session) {
+      // We are always assuming the refresh will succeed, thus we
+      // report the output as available balance.
+      available = Amounts.add(available, session.amountRefreshOutput).amount;
+    } else {
+      available = Amounts.add(available, r.estimatedOutputPerCoin[i]).amount;
+    }
+  }
+  return available;
+}
+
 /**
  * Get balance information.
  */
@@ -110,33 +139,11 @@ export async function getBalancesInsideTransaction(
   });
 
   await tx.refreshGroups.iter().forEach((r) => {
-    // Don't count finished refreshes, since the refresh already resulted
-    // in coins being added to the wallet.
-    if (r.timestampFinished) {
-      return;
-    }
-    for (let i = 0; i < r.oldCoinPubs.length; i++) {
-      const session = r.refreshSessionPerCoin[i];
-      if (session) {
-        const currency = Amounts.parseOrThrow(
-          session.amountRefreshOutput,
-        ).currency;
-        const b = initBalance(currency);
-        // We are always assuming the refresh will succeed, thus we
-        // report the output as available balance.
-        b.available = Amounts.add(
-          b.available,
-          session.amountRefreshOutput,
-        ).amount;
-      } else {
-        const currency = Amounts.parseOrThrow(r.inputPerCoin[i]).currency;
-        const b = initBalance(currency);
-        b.available = Amounts.add(
-          b.available,
-          r.estimatedOutputPerCoin[i],
-        ).amount;
-      }
-    }
+    const b = initBalance(r.currency);
+    b.available = Amounts.add(
+      b.available,
+      computeRefreshGroupAvailableAmount(r),
+    ).amount;
   });
 
   await tx.withdrawalGroups.iter().forEach((wds) => {
@@ -194,3 +201,217 @@ export async function getBalances(
 
   return wbal;
 }
+
+/**
+ * Information about the balance for a particular payment to a particular
+ * merchant.
+ */
+export interface MerchantPaymentBalanceDetails {
+  balanceAvailable: AmountJson;
+}
+
+export interface MerchantPaymentRestrictionsForBalance {
+  currency: string;
+  minAge: number;
+  acceptedExchanges: AllowedExchangeInfo[];
+  acceptedAuditors: AllowedAuditorInfo[];
+  acceptedWireMethods: string[];
+}
+
+export interface AcceptableExchanges {
+  /**
+   * Exchanges accepted by the merchant, but wire method might not match.
+   */
+  acceptableExchanges: string[];
+
+  /**
+   * Exchanges accepted by the merchant, including a matching
+   * wire method, i.e. the merchant can deposit coins there.
+   */
+  depositableExchanges: string[];
+}
+
+/**
+ * Get all exchanges that are acceptable for a particular payment.
+ */
+export async function getAcceptableExchangeBaseUrls(
+  ws: InternalWalletState,
+  req: MerchantPaymentRestrictionsForBalance,
+): Promise<AcceptableExchanges> {
+  const acceptableExchangeUrls = new Set<string>();
+  const depositableExchangeUrls = new Set<string>();
+  await ws.db
+    .mktx((x) => [x.exchanges, x.exchangeDetails, x.auditorTrust])
+    .runReadOnly(async (tx) => {
+      // FIXME: We should have a DB index to look up all exchanges
+      // for a particular auditor ...
+
+      const canonExchanges = new Set<string>();
+      const canonAuditors = new Set<string>();
+
+      for (const exchangeHandle of req.acceptedExchanges) {
+        const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
+        canonExchanges.add(normUrl);
+      }
+
+      for (const auditorHandle of req.acceptedAuditors) {
+        const normUrl = canonicalizeBaseUrl(auditorHandle.auditorBaseUrl);
+        canonAuditors.add(normUrl);
+      }
+
+      await tx.exchanges.iter().forEachAsync(async (exchange) => {
+        const dp = exchange.detailsPointer;
+        if (!dp) {
+          return;
+        }
+        const { currency, masterPublicKey } = dp;
+        const exchangeDetails = await 
tx.exchangeDetails.indexes.byPointer.get([
+          exchange.baseUrl,
+          currency,
+          masterPublicKey,
+        ]);
+        if (!exchangeDetails) {
+          return;
+        }
+
+        let acceptable = false;
+
+        if (canonExchanges.has(exchange.baseUrl)) {
+          acceptableExchangeUrls.add(exchange.baseUrl);
+          acceptable = true;
+        }
+        for (const exchangeAuditor of exchangeDetails.auditors) {
+          if (canonAuditors.has(exchangeAuditor.auditor_url)) {
+            acceptableExchangeUrls.add(exchange.baseUrl);
+            acceptable = true;
+            break;
+          }
+        }
+
+        if (!acceptable) {
+          return;
+        }
+        // FIXME: Also consider exchange and auditor public key
+        // instead of just base URLs?
+
+        let wireMethodSupported = false;
+        for (const acc of exchangeDetails.wireInfo.accounts) {
+          const pp = parsePaytoUri(acc.payto_uri);
+          checkLogicInvariant(!!pp);
+          for (const wm of req.acceptedWireMethods) {
+            if (pp.targetType === wm) {
+              wireMethodSupported = true;
+              break;
+            }
+            if (wireMethodSupported) {
+              break;
+            }
+          }
+        }
+
+        acceptableExchangeUrls.add(exchange.baseUrl);
+        if (wireMethodSupported) {
+          depositableExchangeUrls.add(exchange.baseUrl);
+        }
+      });
+    });
+  return {
+    acceptableExchanges: [...acceptableExchangeUrls],
+    depositableExchanges: [...depositableExchangeUrls],
+  };
+}
+
+export interface MerchantPaymentBalanceDetails {
+  /**
+   * Balance of type "available" (see balance.ts for definition).
+   */
+  balanceAvailable: AmountJson;
+
+  /**
+   * Balance of type "material" (see balance.ts for definition).
+   */
+  balanceMaterial: AmountJson;
+
+  /**
+   * Balance of type "age-acceptable" (see balance.ts for definition).
+   */
+  balanceAgeAcceptable: AmountJson;
+
+  /**
+   * Balance of type "merchant-acceptable" (see balance.ts for definition).
+   */
+  balanceMerchantAcceptable: AmountJson;
+
+  /**
+   * Balance of type "merchant-depositable" (see balance.ts for definition).
+   */
+  balanceMerchantDepositable: AmountJson;
+}
+
+export async function getMerchantPaymentBalanceDetails(
+  ws: InternalWalletState,
+  req: MerchantPaymentRestrictionsForBalance,
+): Promise<MerchantPaymentBalanceDetails> {
+  const acceptability = await getAcceptableExchangeBaseUrls(ws, req);
+
+  const d: MerchantPaymentBalanceDetails = {
+    balanceAvailable: Amounts.zeroOfCurrency(req.currency),
+    balanceMaterial: Amounts.zeroOfCurrency(req.currency),
+    balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
+    balanceMerchantAcceptable: Amounts.zeroOfCurrency(req.currency),
+    balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
+  };
+
+  const wbal = await ws.db
+    .mktx((x) => [
+      x.coins,
+      x.coinAvailability,
+      x.refreshGroups,
+      x.purchases,
+      x.withdrawalGroups,
+    ])
+    .runReadOnly(async (tx) => {
+      await tx.coinAvailability.iter().forEach((ca) => {
+        const singleCoinAmount: AmountJson = {
+          currency: ca.currency,
+          fraction: ca.amountFrac,
+          value: ca.amountVal,
+        };
+        const coinAmount: AmountJson = Amounts.mult(
+          singleCoinAmount,
+          ca.freshCoinCount,
+        ).amount;
+        d.balanceAvailable = Amounts.add(d.balanceAvailable, 
coinAmount).amount;
+        d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
+        if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
+          d.balanceAgeAcceptable = Amounts.add(
+            d.balanceAgeAcceptable,
+            coinAmount,
+          ).amount;
+          if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
+            d.balanceMerchantAcceptable = Amounts.add(
+              d.balanceMerchantAcceptable,
+              coinAmount,
+            ).amount;
+            if (
+              acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)
+            ) {
+              d.balanceMerchantDepositable = Amounts.add(
+                d.balanceMerchantDepositable,
+                coinAmount,
+              ).amount;
+            }
+          }
+        }
+      });
+
+      await tx.refreshGroups.iter().forEach((r) => {
+        d.balanceAvailable = Amounts.add(
+          d.balanceAvailable,
+          computeRefreshGroupAvailableAmount(r),
+        ).amount;
+      });
+    });
+
+  return d;
+}
diff --git a/packages/taler-wallet-core/src/operations/common.ts 
b/packages/taler-wallet-core/src/operations/common.ts
index 2323cb82c..cb22105e1 100644
--- a/packages/taler-wallet-core/src/operations/common.ts
+++ b/packages/taler-wallet-core/src/operations/common.ts
@@ -175,9 +175,11 @@ export async function spendCoins(
     await tx.coins.put(coin);
     await tx.coinAvailability.put(coinAvailability);
   }
+  
   await ws.refreshOps.createRefreshGroup(
     ws,
     tx,
+    Amounts.currencyOf(csi.contributions[0]),
     refreshCoinPubs,
     RefreshReason.PayMerchant,
   );
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 406d658af..1cb051365 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -268,7 +268,7 @@ export async function getFeeForDeposit(
     prevPayCoins: [],
   });
 
-  if (!payCoinSel) {
+  if (payCoinSel.type !== "success") {
     throw Error("insufficient funds");
   }
 
@@ -276,7 +276,7 @@ export async function getFeeForDeposit(
     ws,
     p.targetType,
     amount,
-    payCoinSel,
+    payCoinSel.coinSel,
   );
 }
 
@@ -355,16 +355,16 @@ export async function prepareDepositGroup(
     prevPayCoins: [],
   });
 
-  if (!payCoinSel) {
+  if (payCoinSel.type !== "success") {
     throw Error("insufficient funds");
   }
 
-  const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
+  const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
 
   const effectiveDepositAmount = await getEffectiveDepositAmount(
     ws,
     p.targetType,
-    payCoinSel,
+    payCoinSel.coinSel,
   );
 
   return {
@@ -452,18 +452,18 @@ export async function createDepositGroup(
     prevPayCoins: [],
   });
 
-  if (!payCoinSel) {
+  if (payCoinSel.type !== "success") {
     throw Error("insufficient funds");
   }
 
-  const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
+  const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
 
   const depositGroupId = encodeCrock(getRandomBytes(32));
 
   const effectiveDepositAmount = await getEffectiveDepositAmount(
     ws,
     p.targetType,
-    payCoinSel,
+    payCoinSel.coinSel,
   );
 
   const depositGroup: DepositGroupRecord = {
@@ -474,9 +474,9 @@ export async function createDepositGroup(
     noncePub: noncePair.pub,
     timestampCreated: AbsoluteTime.toTimestamp(now),
     timestampFinished: undefined,
-    payCoinSelection: payCoinSel,
+    payCoinSelection: payCoinSel.coinSel,
     payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
-    depositedPerCoin: payCoinSel.coinPubs.map(() => false),
+    depositedPerCoin: payCoinSel.coinSel.coinPubs.map(() => false),
     merchantPriv: merchantPair.priv,
     merchantPub: merchantPair.pub,
     totalPayCost: Amounts.stringify(totalDepositCost),
@@ -500,8 +500,8 @@ export async function createDepositGroup(
     .runReadWrite(async (tx) => {
       await spendCoins(ws, tx, {
         allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
-        coinPubs: payCoinSel.coinPubs,
-        contributions: payCoinSel.coinContributions.map((x) =>
+        coinPubs: payCoinSel.coinSel.coinPubs,
+        contributions: payCoinSel.coinSel.coinContributions.map((x) =>
           Amounts.parseOrThrow(x),
         ),
         refreshReason: RefreshReason.PayDeposit,
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts 
b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index 05da0a020..6026e0860 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -73,6 +73,7 @@ import {
   TransactionType,
   URL,
   constructPayUri,
+  PayMerchantInsufficientBalanceDetails,
 } from "@gnu-taler/taler-util";
 import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
 import {
@@ -131,11 +132,12 @@ import {
 import { getExchangeDetails } from "./exchanges.js";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
 import { GetReadOnlyAccess } from "../util/query.js";
+import { getMerchantPaymentBalanceDetails } from "./balance.js";
 
 /**
  * Logger.
  */
-const logger = new Logger("pay.ts");
+const logger = new Logger("pay-merchant.ts");
 
 /**
  * Compute the total cost of a payment to the customer.
@@ -817,7 +819,7 @@ async function handleInsufficientFunds(
     requiredMinimumAge: contractData.minimumAge,
   });
 
-  if (!res) {
+  if (res.type !== "success") {
     logger.trace("insufficient funds for coin re-selection");
     return;
   }
@@ -841,8 +843,7 @@ async function handleInsufficientFunds(
       if (!payInfo) {
         return;
       }
-      payInfo.payCoinSelection = res;
-      payInfo.payCoinSelection = res;
+      payInfo.payCoinSelection = res.coinSel;
       payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
       await tx.purchases.put(p);
       await spendCoins(ws, tx, {
@@ -905,6 +906,8 @@ export async function selectCandidates(
       x.coinAvailability,
     ])
     .runReadOnly(async (tx) => {
+      // FIXME: Use the existing helper (from balance.ts) to
+      // get acceptable exchanges.
       const denoms: AvailableDenom[] = [];
       const exchanges = await tx.exchanges.iter().toArray();
       const wfPerExchange: Record<string, AmountJson> = {};
@@ -1030,6 +1033,7 @@ export function selectGreedy(
       // Don't use this coin if depositing it is more expensive than
       // the amount it would give the merchant.
       if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
+        tally.lastDepositFee = Amounts.parseOrThrow(aci.feeDeposit);
         continue;
       }
 
@@ -1129,6 +1133,13 @@ export function selectForced(
   return selectedDenom;
 }
 
+export type SelectPayCoinsResult =
+  | {
+      type: "failure";
+      insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
+    }
+  | { type: "success"; coinSel: PayCoinSelection };
+
 /**
  * Given a list of candidate coins, select coins to spend under the merchant's
  * constraints.
@@ -1142,7 +1153,7 @@ export function selectForced(
 export async function selectPayCoinsNew(
   ws: InternalWalletState,
   req: SelectPayCoinRequestNg,
-): Promise<PayCoinSelection | undefined> {
+): Promise<SelectPayCoinsResult> {
   const {
     contractTermsAmount,
     depositFeeLimit,
@@ -1168,6 +1179,7 @@ export async function selectPayCoinsNew(
     customerDepositFees: Amounts.zeroOfCurrency(currency),
     customerWireFees: Amounts.zeroOfCurrency(currency),
     wireFeeCoveredForExchange: new Set(),
+    lastDepositFee: Amounts.zeroOfCurrency(currency),
   };
 
   const prevPayCoins = req.prevPayCoins ?? [];
@@ -1207,7 +1219,44 @@ export async function selectPayCoinsNew(
   }
 
   if (!selectedDenom) {
-    return undefined;
+    const details = await getMerchantPaymentBalanceDetails(ws, {
+      acceptedAuditors: req.auditors,
+      acceptedExchanges: req.exchanges,
+      acceptedWireMethods: [req.wireMethod],
+      currency: Amounts.currencyOf(req.contractTermsAmount),
+      minAge: req.requiredMinimumAge ?? 0,
+    });
+    let feeGapEstimate: AmountJson;
+    if (
+      Amounts.cmp(
+        details.balanceMerchantDepositable,
+        req.contractTermsAmount,
+      ) >= 0
+    ) {
+      // FIXME: We can probably give a better estimate.
+      feeGapEstimate = Amounts.add(
+        tally.amountPayRemaining,
+        tally.lastDepositFee,
+      ).amount;
+    } else {
+      feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount);
+    }
+    return {
+      type: "failure",
+      insufficientBalanceDetails: {
+        amountRequested: Amounts.stringify(req.contractTermsAmount),
+        balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
+        balanceAvailable: Amounts.stringify(details.balanceAvailable),
+        balanceMaterial: Amounts.stringify(details.balanceMaterial),
+        balanceMerchantAcceptable: Amounts.stringify(
+          details.balanceMerchantAcceptable,
+        ),
+        balanceMerchantDepositable: Amounts.stringify(
+          details.balanceMerchantDepositable,
+        ),
+        feeGapEstimate: Amounts.stringify(feeGapEstimate),
+      },
+    };
   }
 
   const finalSel = selectedDenom;
@@ -1244,11 +1293,14 @@ export async function selectPayCoinsNew(
     });
 
   return {
-    paymentAmount: Amounts.stringify(contractTermsAmount),
-    coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
-    coinPubs,
-    customerDepositFees: Amounts.stringify(tally.customerDepositFees),
-    customerWireFees: Amounts.stringify(tally.customerWireFees),
+    type: "success",
+    coinSel: {
+      paymentAmount: Amounts.stringify(contractTermsAmount),
+      coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
+      coinPubs,
+      customerDepositFees: Amounts.stringify(tally.customerDepositFees),
+      customerWireFees: Amounts.stringify(tally.customerWireFees),
+    },
   };
 }
 
@@ -1318,7 +1370,7 @@ export async function checkPaymentByProposalId(
       wireMethod: contractData.wireMethod,
     });
 
-    if (!res) {
+    if (res.type !== "success") {
       logger.info("not allowing payment, insufficient coins");
       return {
         status: PreparePayResultType.InsufficientBalance,
@@ -1327,10 +1379,11 @@ export async function checkPaymentByProposalId(
         noncePriv: proposal.noncePriv,
         amountRaw: Amounts.stringify(d.contractData.amount),
         talerUri,
+        balanceDetails: res.insufficientBalanceDetails,
       };
     }
 
-    const totalCost = await getTotalPaymentCost(ws, res);
+    const totalCost = await getTotalPaymentCost(ws, res.coinSel);
     logger.trace("costInfo", totalCost);
     logger.trace("coinsForPayment", res);
 
@@ -1340,7 +1393,7 @@ export async function checkPaymentByProposalId(
       proposalId: proposal.proposalId,
       noncePriv: proposal.noncePriv,
       amountEffective: Amounts.stringify(totalCost),
-      amountRaw: Amounts.stringify(res.paymentAmount),
+      amountRaw: Amounts.stringify(res.coinSel.paymentAmount),
       contractTermsHash: d.contractData.contractTermsHash,
       talerUri,
     };
@@ -1666,9 +1719,9 @@ export async function confirmPay(
 
   const contractData = d.contractData;
 
-  let maybeCoinSelection: PayCoinSelection | undefined = undefined;
+  let selectCoinsResult: SelectPayCoinsResult | undefined = undefined;
 
-  maybeCoinSelection = await selectPayCoinsNew(ws, {
+  selectCoinsResult = await selectPayCoinsNew(ws, {
     auditors: contractData.allowedAuditors,
     exchanges: contractData.allowedExchanges,
     wireMethod: contractData.wireMethod,
@@ -1681,9 +1734,9 @@ export async function confirmPay(
     forcedSelection: forcedCoinSel,
   });
 
-  logger.trace("coin selection result", maybeCoinSelection);
+  logger.trace("coin selection result", selectCoinsResult);
 
-  if (!maybeCoinSelection) {
+  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.
@@ -1691,14 +1744,7 @@ export async function confirmPay(
     throw Error("insufficient balance");
   }
 
-  const coinSelection = maybeCoinSelection;
-
-  const depositPermissions = await generateDepositPermissions(
-    ws,
-    coinSelection,
-    d.contractData,
-  );
-
+  const coinSelection = selectCoinsResult.coinSel;
   const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
 
   let sessionId: string | undefined;
@@ -2373,6 +2419,7 @@ async function acceptRefunds(
         await createRefreshGroup(
           ws,
           tx,
+          Amounts.currencyOf(refreshCoinsPubs[0].amount),
           refreshCoinsPubs,
           RefreshReason.Refund,
         );
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 4feb4430d..00dd0e1c6 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -429,6 +429,7 @@ export async function processRecoupGroupHandler(
         const refreshGroupId = await createRefreshGroup(
           ws,
           tx,
+          Amounts.currencyOf(rg2.scheduleRefreshCoins[0].amount),
           rg2.scheduleRefreshCoins,
           RefreshReason.Recoup,
         );
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index eeff84be6..638dec8a6 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -850,6 +850,7 @@ export async function createRefreshGroup(
     refreshGroups: typeof WalletStoresV1.refreshGroups;
     coinAvailability: typeof WalletStoresV1.coinAvailability;
   }>,
+  currency: string,
   oldCoinPubs: CoinRefreshRequest[],
   reason: RefreshReason,
 ): Promise<RefreshGroupId> {
@@ -934,6 +935,7 @@ export async function createRefreshGroup(
 
   const refreshGroup: RefreshGroupRecord = {
     operationStatus: RefreshOperationStatus.Pending,
+    currency,
     timestampFinished: undefined,
     statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
     oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
@@ -1018,7 +1020,7 @@ export async function autoRefresh(
     ])
     .runReadWrite(async (tx) => {
       const exchange = await tx.exchanges.get(exchangeBaseUrl);
-      if (!exchange) {
+      if (!exchange || !exchange.detailsPointer) {
         return;
       }
       const coins = await tx.coins.indexes.byBaseUrl
@@ -1059,6 +1061,7 @@ export async function autoRefresh(
         const res = await createRefreshGroup(
           ws,
           tx,
+          exchange.detailsPointer?.currency,
           refreshCoins,
           RefreshReason.Scheduled,
         );
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index cadf8d829..0bd624bf7 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -117,6 +117,8 @@ export interface CoinSelectionTally {
   customerWireFees: AmountJson;
 
   wireFeeCoveredForExchange: Set<string>;
+
+  lastDepositFee: AmountJson;
 }
 
 /**
@@ -188,5 +190,6 @@ export function tallyFees(
     customerDepositFees,
     customerWireFees,
     wireFeeCoveredForExchange,
+    lastDepositFee: feeDeposit,
   };
 }
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 1defff0d2..e15c6110c 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -1178,6 +1178,9 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
     }
     case WalletApiOperation.ForceRefresh: {
       const req = codecForForceRefreshRequest().decode(payload);
+      if (req.coinPubList.length == 0) {
+        throw Error("refusing to create empty refresh group");
+      }
       const refreshGroupId = await ws.db
         .mktx((x) => [
           x.refreshGroups,
@@ -1207,6 +1210,7 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
           return await createRefreshGroup(
             ws,
             tx,
+            Amounts.currencyOf(coinPubs[0].amount),
             coinPubs,
             RefreshReason.Manual,
           );

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