gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: refactor coin selection, repo


From: gnunet
Subject: [taler-wallet-core] branch master updated: refactor coin selection, report maxEffectiveSpendAmount
Date: Thu, 07 Mar 2024 00:06:11 +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 7ba1d1f33 refactor coin selection, report maxEffectiveSpendAmount
7ba1d1f33 is described below

commit 7ba1d1f3351e58a331e99337afea0fbedb6eb828
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Mar 6 21:15:30 2024 +0100

    refactor coin selection, report maxEffectiveSpendAmount
---
 .../src/integrationtests/test-forced-selection.ts  |   2 +-
 packages/taler-util/src/wallet-types.ts            |  39 +--
 .../taler-wallet-core/src/coinSelection.test.ts    |   2 +-
 packages/taler-wallet-core/src/coinSelection.ts    | 366 ++++++++++-----------
 packages/taler-wallet-core/src/db.ts               |  14 +-
 packages/taler-wallet-core/src/deposits.ts         |  31 +-
 .../src/instructedAmountConversion.test.ts         |   4 +-
 .../src/instructedAmountConversion.ts              |  17 +-
 packages/taler-wallet-core/src/pay-merchant.ts     |  63 ++--
 packages/taler-wallet-core/src/pay-peer-common.ts  |   8 +-
 .../taler-wallet-core/src/pay-peer-pull-debit.ts   |  14 +-
 .../taler-wallet-core/src/pay-peer-push-debit.ts   |  14 +-
 packages/taler-wallet-core/src/testing.ts          |  10 +-
 .../src/components/PaymentButtons.tsx              |  22 +-
 .../src/cta/Payment/stories.tsx                    |  12 +-
 .../src/cta/TransferCreate/state.ts                |  18 +-
 16 files changed, 307 insertions(+), 329 deletions(-)

diff --git 
a/packages/taler-harness/src/integrationtests/test-forced-selection.ts 
b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
index 752810703..839ddd927 100644
--- a/packages/taler-harness/src/integrationtests/test-forced-selection.ts
+++ b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
@@ -80,7 +80,7 @@ export async function runForcedSelectionTest(t: 
GlobalTestState) {
   console.log(j2s(payResp));
 
   // Without forced selection, we would only use 2 coins.
-  t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3);
+  t.assertDeepEqual(payResp.numCoins, 3);
 }
 
 runForcedSelectionTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index 9fe114b3d..cb4374648 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -868,21 +868,15 @@ export interface PayMerchantInsufficientBalanceDetails {
   balanceMerchantDepositable: AmountString;
 
   /**
-   * If the payment would succeed without fees
-   * (i.e. balanceMerchantDepositable >= amountRequested),
-   * this field contains an estimate of the amount that would additionally
-   * be required to cover the fees.
-   *
-   * 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.
+   * Maximum effective amount that the wallet can spend,
+   * when all fees are paid by the wallet.
    */
-  feeGapEstimate: AmountString;
+  maxEffectiveSpendAmount: AmountString;
 
   perExchange: {
     [url: string]: {
       balanceAvailable: AmountString;
       balanceMaterial: AmountString;
-      feeGapEstimate: AmountString;
     };
   };
 }
@@ -896,8 +890,8 @@ export const codecForPayMerchantInsufficientBalanceDetails =
       .property("balanceMaterial", codecForAmountString())
       .property("balanceMerchantAcceptable", codecForAmountString())
       .property("balanceMerchantDepositable", codecForAmountString())
-      .property("feeGapEstimate", codecForAmountString())
       .property("perExchange", codecForAny())
+      .property("maxEffectiveSpendAmount", codecForAmountString())
       .build("PayMerchantInsufficientBalanceDetails");
 
 export const codecForPreparePayResultInsufficientBalance =
@@ -2623,7 +2617,15 @@ export interface ForcedCoinSel {
 }
 
 export interface TestPayResult {
-  payCoinSelection: PayCoinSelection;
+  /**
+   * Number of coins used for the payment.
+   */
+  numCoins: number;
+}
+
+export interface SelectedCoin {
+  coinPub: string;
+  contribution: AmountString;
 }
 
 /**
@@ -2631,20 +2633,7 @@ export interface TestPayResult {
  * coins with their denomination.
  */
 export interface PayCoinSelection {
-  /**
-   * Amount requested by the merchant.
-   */
-  paymentAmount: AmountString;
-
-  /**
-   * Public keys of the coins that were selected.
-   */
-  coinPubs: string[];
-
-  /**
-   * Amount that each coin contributes.
-   */
-  coinContributions: AmountString[];
+  coins: SelectedCoin[];
 
   /**
    * How much of the wire fees is the customer paying?
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts 
b/packages/taler-wallet-core/src/coinSelection.test.ts
index 6eae9deaa..3d8e24b0c 100644
--- a/packages/taler-wallet-core/src/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -179,7 +179,7 @@ test("pay: select one coin to pay with fee", (t) => {
     amountWireFeeLimitRemaining: zero,
     amountDepositFeeLimitRemaining: zero,
     customerDepositFees: Amounts.parse("LOCAL:0.1"),
-    customerWireFees: zero,
+    customerWireFees: Amounts.parse("LOCAL:0.1"),
     wireFeeCoveredForExchange: new Set(["http://exchange.localhost/";]),
     lastDepositFee: Amounts.parse("LOCAL:0.1"),
   });
diff --git a/packages/taler-wallet-core/src/coinSelection.ts 
b/packages/taler-wallet-core/src/coinSelection.ts
index 1208e7c37..5ac52e1d3 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -27,19 +27,15 @@ import { GlobalIDB } from "@gnu-taler/idb-bridge";
 import {
   AbsoluteTime,
   AccountRestriction,
-  AgeCommitmentProof,
   AgeRestriction,
   AllowedAuditorInfo,
   AllowedExchangeInfo,
   AmountJson,
   Amounts,
-  AmountString,
   checkDbInvariant,
   checkLogicInvariant,
-  CoinPublicKeyString,
   CoinStatus,
   DenominationInfo,
-  Duration,
   ForcedCoinSel,
   InternationalizedString,
   j2s,
@@ -47,9 +43,9 @@ import {
   parsePaytoUri,
   PayCoinSelection,
   PayMerchantInsufficientBalanceDetails,
+  SelectedCoin,
   strcmp,
   TalerProtocolTimestamp,
-  UnblindedSignature,
 } from "@gnu-taler/taler-util";
 import {
   getExchangePaymentBalanceDetailsInTx,
@@ -68,8 +64,6 @@ const logger = new Logger("coinSelection.ts");
 export type PreviousPayCoins = {
   coinPub: string;
   contribution: AmountJson;
-  feeDeposit: AmountJson;
-  exchangeBaseUrl: string;
 }[];
 
 export interface ExchangeRestrictionSpec {
@@ -192,12 +186,7 @@ export async function selectPayCoins(
   wex: WalletExecutionContext,
   req: SelectPayCoinRequestNg,
 ): Promise<SelectPayCoinsResult> {
-  const {
-    contractTermsAmount,
-    depositFeeLimit,
-    wireFeeLimit,
-    wireFeeAmortization,
-  } = req;
+  const { contractTermsAmount, depositFeeLimit, wireFeeLimit } = req;
 
   return await wex.db.runReadOnlyTx(
     [
@@ -221,8 +210,7 @@ export async function selectPayCoins(
         },
       );
 
-      const coinPubs: string[] = [];
-      const coinContributions: AmountJson[] = [];
+      const coinRes: SelectedCoin[] = [];
       const currency = contractTermsAmount.currency;
 
       let tally: CoinSelectionTally = {
@@ -235,25 +223,17 @@ export async function selectPayCoins(
         lastDepositFee: Amounts.zeroOfCurrency(currency),
       };
 
-      const prevPayCoins = req.prevPayCoins ?? [];
-
-      // Look at existing pay coin selection and tally up
-      for (const prev of prevPayCoins) {
-        tallyFees(
-          tally,
-          wireFeesPerExchange,
-          wireFeeAmortization,
-          prev.exchangeBaseUrl,
-          prev.feeDeposit,
-        );
-        tally.amountPayRemaining = Amounts.sub(
-          tally.amountPayRemaining,
-          prev.contribution,
-        ).amount;
-
-        coinPubs.push(prev.coinPub);
-        coinContributions.push(prev.contribution);
-      }
+      await maybeRepairCoinSelection(
+        wex,
+        tx,
+        req.prevPayCoins ?? [],
+        coinRes,
+        tally,
+        {
+          wireFeeAmortization: req.wireFeeAmortization,
+          wireFeesPerExchange: wireFeesPerExchange,
+        },
+      );
 
       let selectedDenom: SelResult | undefined;
       if (req.forcedSelection) {
@@ -292,8 +272,7 @@ export async function selectPayCoins(
       const coinSel = await assembleSelectPayCoinsSuccessResult(
         tx,
         selectedDenom,
-        coinPubs,
-        coinContributions,
+        coinRes,
         req.contractTermsAmount,
         tally,
       );
@@ -306,11 +285,55 @@ export async function selectPayCoins(
   );
 }
 
+async function maybeRepairCoinSelection(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+  prevPayCoins: PreviousPayCoins,
+  coinRes: SelectedCoin[],
+  tally: CoinSelectionTally,
+  feeInfo: {
+    wireFeeAmortization: number;
+    wireFeesPerExchange: Record<string, AmountJson>;
+  },
+): Promise<void> {
+  // Look at existing pay coin selection and tally up
+  for (const prev of prevPayCoins) {
+    const coin = await tx.coins.get(prev.coinPub);
+    if (!coin) {
+      continue;
+    }
+    const denom = await getDenomInfo(
+      wex,
+      tx,
+      coin.exchangeBaseUrl,
+      coin.denomPubHash,
+    );
+    if (!denom) {
+      continue;
+    }
+    tallyFees(
+      tally,
+      feeInfo.wireFeesPerExchange,
+      feeInfo.wireFeeAmortization,
+      coin.exchangeBaseUrl,
+      Amounts.parseOrThrow(denom.feeDeposit),
+    );
+    tally.amountPayRemaining = Amounts.sub(
+      tally.amountPayRemaining,
+      prev.contribution,
+    ).amount;
+
+    coinRes.push({
+      coinPub: prev.coinPub,
+      contribution: Amounts.stringify(prev.contribution),
+    });
+  }
+}
+
 async function assembleSelectPayCoinsSuccessResult(
   tx: WalletDbReadOnlyTransaction<["coins"]>,
   finalSel: SelResult,
-  coinPubs: string[],
-  coinContributions: AmountJson[],
+  coinRes: SelectedCoin[],
   contractTermsAmount: AmountJson,
   tally: CoinSelectionTally,
 ): Promise<PayCoinSelection> {
@@ -334,15 +357,17 @@ async function assembleSelectPayCoinsSuccessResult(
         `coin selection failed (not available anymore, got only 
${coins.length}/${numRequested})`,
       );
     }
-    coinPubs.push(...coins.map((x) => x.coinPub));
-    coinContributions.push(...selInfo.contributions);
+
+    for (let i = 0; i < selInfo.contributions.length; i++) {
+      coinRes.push({
+        coinPub: coins[i].coinPub,
+        contribution: Amounts.stringify(selInfo.contributions[i]),
+      });
+    }
   }
 
   return {
-    // FIXME: Why do we return this?!
-    paymentAmount: Amounts.stringify(contractTermsAmount),
-    coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
-    coinPubs,
+    coins: coinRes,
     customerDepositFees: Amounts.stringify(tally.customerDepositFees),
     customerWireFees: Amounts.stringify(tally.customerWireFees),
   };
@@ -358,7 +383,13 @@ interface ReportInsufficientBalanceRequest {
 export async function reportInsufficientBalanceDetails(
   wex: WalletExecutionContext,
   tx: WalletDbReadOnlyTransaction<
-    ["coinAvailability", "exchanges", "exchangeDetails", "refreshGroups"]
+    [
+      "coinAvailability",
+      "exchanges",
+      "exchangeDetails",
+      "refreshGroups",
+      "denominations",
+    ]
   >,
   req: ReportInsufficientBalanceRequest,
 ): Promise<PayMerchantInsufficientBalanceDetails> {
@@ -384,10 +415,52 @@ export async function reportInsufficientBalanceDetails(
 
   const exchanges = await tx.exchanges.iter().toArray();
 
+  let maxEffectiveSpendAmount = Amounts.zeroOfAmount(req.instructedAmount);
+
   for (const exch of exchanges) {
     if (exch.detailsPointer?.currency !== currency) {
       continue;
     }
+
+    // We now see how much we could spend if we paid all the fees ourselves
+    // in a worst-case estimate.
+
+    const exchangeBaseUrl = exch.baseUrl;
+    let ageLower = 0;
+    let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+    if (req.requiredMinimumAge) {
+      ageLower = req.requiredMinimumAge;
+    }
+
+    const myExchangeCoins =
+      await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+        GlobalIDB.KeyRange.bound(
+          [exchangeBaseUrl, ageLower, 1],
+          [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+        ),
+      );
+
+    for (const ec of myExchangeCoins) {
+      maxEffectiveSpendAmount = Amounts.add(
+        maxEffectiveSpendAmount,
+        Amounts.mult(ec.value, ec.freshCoinCount).amount,
+      ).amount;
+
+      const denom = await getDenomInfo(
+        wex,
+        tx,
+        exchangeBaseUrl,
+        ec.denomPubHash,
+      );
+      if (!denom) {
+        continue;
+      }
+      maxEffectiveSpendAmount = Amounts.sub(
+        maxEffectiveSpendAmount,
+        Amounts.mult(denom.feeDeposit, ec.freshCoinCount).amount,
+      ).amount;
+    }
+
     const infoExchange = await getExchangePaymentBalanceDetailsInTx(wex, tx, {
       currency,
       restrictExchangeTo: exch.baseUrl,
@@ -395,7 +468,6 @@ export async function reportInsufficientBalanceDetails(
     perExchange[exch.baseUrl] = {
       balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
       balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
-      feeGapEstimate: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
     };
   }
 
@@ -410,7 +482,7 @@ export async function reportInsufficientBalanceDetails(
     balanceMerchantDepositable: Amounts.stringify(
       details.balanceMerchantDepositable,
     ),
-    feeGapEstimate: Amounts.stringify(feeGapEstimate),
+    maxEffectiveSpendAmount: Amounts.stringify(maxEffectiveSpendAmount),
     perExchange,
   };
 }
@@ -434,8 +506,6 @@ interface SelResult {
   [avKey: string]: {
     exchangeBaseUrl: string;
     denomPubHash: string;
-    expireWithdraw: TalerProtocolTimestamp;
-    expireDeposit: TalerProtocolTimestamp;
     maxAge: number;
     contributions: AmountJson[];
   };
@@ -508,8 +578,6 @@ function selectGreedy(
           denomPubHash: denom.denomPubHash,
           exchangeBaseUrl: denom.exchangeBaseUrl,
           maxAge: denom.maxAge,
-          expireDeposit: denom.stampExpireDeposit,
-          expireWithdraw: denom.stampExpireWithdraw,
         };
       }
       sd.contributions.push(...contributions);
@@ -549,8 +617,6 @@ function selectForced(
             denomPubHash: aci.denomPubHash,
             exchangeBaseUrl: aci.exchangeBaseUrl,
             maxAge: aci.maxAge,
-            expireDeposit: aci.stampExpireDeposit,
-            expireWithdraw: aci.stampExpireWithdraw,
           };
         }
         sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
@@ -563,7 +629,6 @@ function selectForced(
       throw Error("can't find coin for forced coin selection");
     }
   }
-
   return selectedDenom;
 }
 
@@ -696,12 +761,7 @@ interface SelectPayCandidatesRequest {
   instructedAmount: AmountJson;
   restrictWireMethod: string | undefined;
   depositPaytoUri?: string;
-  restrictExchanges:
-    | {
-        exchanges: AllowedExchangeInfo[];
-        auditors: AllowedAuditorInfo[];
-      }
-    | undefined;
+  restrictExchanges: ExchangeRestrictionSpec | undefined;
   requiredMinimumAge?: number;
 }
 
@@ -796,36 +856,13 @@ async function selectPayCandidates(
   return [denoms, wfPerExchange];
 }
 
-export interface CoinInfo {
-  id: string;
-  value: AmountJson;
-  denomDeposit: AmountJson;
-  denomWithdraw: AmountJson;
-  denomRefresh: AmountJson;
-  totalAvailable: number | undefined;
-  exchangeWire: AmountJson | undefined;
-  exchangePurse: AmountJson | undefined;
-  duration: Duration;
-  exchangeBaseUrl: string;
-  maxAge: number;
-}
-
-export interface SelectedPeerCoin {
-  coinPub: string;
-  coinPriv: string;
-  contribution: AmountString;
-  denomPubHash: string;
-  denomSig: UnblindedSignature;
-  ageCommitmentProof: AgeCommitmentProof | undefined;
-}
-
 export interface PeerCoinSelectionDetails {
   exchangeBaseUrl: string;
 
   /**
    * Info of Coins that were selected.
    */
-  coins: SelectedPeerCoin[];
+  coins: SelectedCoin[];
 
   /**
    * How much of the deposit fees is the customer paying?
@@ -842,12 +879,6 @@ export type SelectPeerCoinsResult =
       insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
     };
 
-export interface PeerCoinRepair {
-  exchangeBaseUrl: string;
-  coinPubs: CoinPublicKeyString[];
-  contribs: AmountJson[];
-}
-
 export interface PeerCoinSelectionRequest {
   instructedAmount: AmountJson;
 
@@ -855,121 +886,39 @@ export interface PeerCoinSelectionRequest {
    * Instruct the coin selection to repair this coin
    * selection instead of selecting completely new coins.
    */
-  repair?: PeerCoinRepair;
+  repair?: PreviousPayCoins;
 }
 
-async function assemblePeerCoinSelectionDetails(
-  tx: WalletDbReadOnlyTransaction<["coins"]>,
-  exchangeBaseUrl: string,
+export async function computeCoinSelMaxExpirationDate(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
   selectedDenom: SelResult,
-  resCoins: ResCoin[],
-  tally: CoinSelectionTally,
-): Promise<PeerCoinSelectionDetails> {
+): Promise<TalerProtocolTimestamp> {
   let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
   for (const dph of Object.keys(selectedDenom)) {
     const selInfo = selectedDenom[dph];
+    const denom = await getDenomInfo(
+      wex,
+      tx,
+      selInfo.exchangeBaseUrl,
+      selInfo.denomPubHash,
+    );
+    if (!denom) {
+      continue;
+    }
     // Compute earliest time that a selected denom
     // would have its coins auto-refreshed.
     minAutorefreshExecuteThreshold = TalerProtocolTimestamp.min(
       minAutorefreshExecuteThreshold,
       AbsoluteTime.toProtocolTimestamp(
         getAutoRefreshExecuteThreshold({
-          stampExpireDeposit: selInfo.expireDeposit,
-          stampExpireWithdraw: selInfo.expireWithdraw,
+          stampExpireDeposit: denom.stampExpireDeposit,
+          stampExpireWithdraw: denom.stampExpireWithdraw,
         }),
       ),
     );
-    const numRequested = selInfo.contributions.length;
-    const query = [
-      selInfo.exchangeBaseUrl,
-      selInfo.denomPubHash,
-      selInfo.maxAge,
-      CoinStatus.Fresh,
-    ];
-    logger.info(`query: ${j2s(query)}`);
-    const coins =
-      await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
-        query,
-        numRequested,
-      );
-    if (coins.length != numRequested) {
-      throw Error(
-        `coin selection failed (not available anymore, got only 
${coins.length}/${numRequested})`,
-      );
-    }
-    for (let i = 0; i < selInfo.contributions.length; i++) {
-      resCoins.push({
-        coinPriv: coins[i].coinPriv,
-        coinPub: coins[i].coinPub,
-        contribution: Amounts.stringify(selInfo.contributions[i]),
-        ageCommitmentProof: coins[i].ageCommitmentProof,
-        denomPubHash: selInfo.denomPubHash,
-        denomSig: coins[i].denomSig,
-      });
-    }
   }
-
-  return {
-    exchangeBaseUrl,
-    coins: resCoins,
-    depositFees: tally.customerDepositFees,
-    maxExpirationDate: minAutorefreshExecuteThreshold,
-  };
-}
-
-async function maybeRepairPeerCoinSelection(
-  wex: WalletExecutionContext,
-  tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
-  exchangeBaseUrl: string,
-  tally: CoinSelectionTally,
-  repair: PeerCoinRepair | undefined,
-): Promise<ResCoin[]> {
-  const resCoins: ResCoin[] = [];
-
-  if (repair && repair.exchangeBaseUrl === exchangeBaseUrl) {
-    for (let i = 0; i < repair.coinPubs.length; i++) {
-      const contrib = repair.contribs[i];
-      const coin = await tx.coins.get(repair.coinPubs[i]);
-      if (!coin) {
-        throw Error("repair not possible, coin not found");
-      }
-      const denom = await getDenomInfo(
-        wex,
-        tx,
-        coin.exchangeBaseUrl,
-        coin.denomPubHash,
-      );
-      checkDbInvariant(!!denom);
-      resCoins.push({
-        coinPriv: coin.coinPriv,
-        coinPub: coin.coinPub,
-        contribution: Amounts.stringify(contrib),
-        denomPubHash: coin.denomPubHash,
-        denomSig: coin.denomSig,
-        ageCommitmentProof: coin.ageCommitmentProof,
-      });
-      const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
-      tally.lastDepositFee = depositFee;
-      tally.amountPayRemaining = Amounts.sub(
-        tally.amountPayRemaining,
-        Amounts.sub(contrib, depositFee).amount,
-      ).amount;
-      tally.customerDepositFees = Amounts.add(
-        tally.customerDepositFees,
-        depositFee,
-      ).amount;
-    }
-  }
-  return resCoins;
-}
-
-interface ResCoin {
-  coinPub: string;
-  coinPriv: string;
-  contribution: AmountString;
-  denomPubHash: string;
-  denomSig: UnblindedSignature;
-  ageCommitmentProof: AgeCommitmentProof | undefined;
+  return minAutorefreshExecuteThreshold;
 }
 
 export function emptyTallyForPeerPayment(
@@ -1034,12 +983,18 @@ export async function selectPeerCoins(
           logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
         }
         const tally = emptyTallyForPeerPayment(req.instructedAmount);
-        const resCoins: ResCoin[] = await maybeRepairPeerCoinSelection(
+        const resCoins: SelectedCoin[] = [];
+
+        await maybeRepairCoinSelection(
           wex,
           tx,
-          exch.baseUrl,
+          req.repair ?? [],
+          resCoins,
           tally,
-          req.repair,
+          {
+            wireFeeAmortization: 1,
+            wireFeesPerExchange: {},
+          },
         );
 
         if (logger.shouldLogTrace()) {
@@ -1058,15 +1013,28 @@ export async function selectPeerCoins(
         );
 
         if (selectedDenom) {
+          const r = await assembleSelectPayCoinsSuccessResult(
+            tx,
+            selectedDenom,
+            resCoins,
+            req.instructedAmount,
+            tally,
+          );
+
+          const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+            wex,
+            tx,
+            selectedDenom,
+          );
+
           return {
             type: "success",
-            result: await assemblePeerCoinSelectionDetails(
-              tx,
-              exch.baseUrl,
-              selectedDenom,
-              resCoins,
-              tally,
-            ),
+            result: {
+              coins: r.coins,
+              depositFees: Amounts.parseOrThrow(r.customerDepositFees),
+              exchangeBaseUrl: exch.baseUrl,
+              maxExpirationDate,
+            },
           };
         }
       }
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index dabc6393d..14621c2d5 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -48,7 +48,6 @@ import {
   ExchangeGlobalFees,
   HashCodeString,
   Logger,
-  PayCoinSelection,
   RefreshReason,
   TalerErrorDetail,
   TalerPreciseTimestamp,
@@ -1207,8 +1206,13 @@ export interface ProposalDownloadInfo {
   contractTermsMerchantSig: string;
 }
 
+export interface DbCoinSelection {
+  coinPubs: string[];
+  coinContributions: AmountString[];
+}
+
 export interface PurchasePayInfo {
-  payCoinSelection: PayCoinSelection;
+  payCoinSelection: DbCoinSelection;
   totalPayCost: AmountString;
   payCoinSelectionUid: string;
 }
@@ -1769,7 +1773,7 @@ export interface DepositGroupRecord {
 
   contractTermsHash: string;
 
-  payCoinSelection: PayCoinSelection;
+  payCoinSelection: DbCoinSelection;
 
   payCoinSelectionUid: string;
 
@@ -1847,7 +1851,7 @@ export enum PeerPushDebitStatus {
   Expired = 0x0502_0000,
 }
 
-export interface PeerPushPaymentCoinSelection {
+export interface DbPeerPushPaymentCoinSelection {
   contributions: AmountString[];
   coinPubs: CoinPublicKeyString[];
 }
@@ -1868,7 +1872,7 @@ export interface PeerPushDebitRecord {
 
   totalCost: AmountString;
 
-  coinSel: PeerPushPaymentCoinSelection;
+  coinSel: DbPeerPushPaymentCoinSelection;
 
   contractTermsHash: HashCodeString;
 
diff --git a/packages/taler-wallet-core/src/deposits.ts 
b/packages/taler-wallet-core/src/deposits.ts
index 2e28ba9b7..2c7ee3596 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -1378,8 +1378,8 @@ export async function createDepositGroup(
   const infoPerExchange: Record<string, DepositInfoPerExchange> = {};
 
   await wex.db.runReadOnlyTx(["coins"], async (tx) => {
-    for (let i = 0; i < payCoinSel.coinSel.coinPubs.length; i++) {
-      const coin = await tx.coins.get(payCoinSel.coinSel.coinPubs[i]);
+    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;
@@ -1392,7 +1392,7 @@ export async function createDepositGroup(
           ),
         };
       }
-      const contrib = payCoinSel.coinSel.coinContributions[i];
+      const contrib = payCoinSel.coinSel.coins[i].contribution;
       depPerExchange.amountEffective = Amounts.stringify(
         Amounts.add(depPerExchange.amountEffective, contrib).amount,
       );
@@ -1417,10 +1417,13 @@ export async function createDepositGroup(
       AbsoluteTime.toPreciseTimestamp(now),
     ),
     timestampFinished: undefined,
-    statusPerCoin: payCoinSel.coinSel.coinPubs.map(
+    statusPerCoin: payCoinSel.coinSel.coins.map(
       () => DepositElementStatus.DepositPending,
     ),
-    payCoinSelection: payCoinSel.coinSel,
+    payCoinSelection: {
+      coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution),
+      coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+    },
     payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
     merchantPriv: merchantPair.priv,
     merchantPub: merchantPair.pub,
@@ -1455,9 +1458,9 @@ export async function createDepositGroup(
     async (tx) => {
       await spendCoins(wex, tx, {
         allocationId: transactionId,
-        coinPubs: payCoinSel.coinSel.coinPubs,
-        contributions: payCoinSel.coinSel.coinContributions.map((x) =>
-          Amounts.parseOrThrow(x),
+        coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub),
+        contributions: payCoinSel.coinSel.coins.map((x) =>
+          Amounts.parseOrThrow(x.contribution),
         ),
         refreshReason: RefreshReason.PayDeposit,
       });
@@ -1508,8 +1511,8 @@ export async function 
getCounterpartyEffectiveDepositAmount(
   await wex.db.runReadOnlyTx(
     ["coins", "denominations", "exchangeDetails", "exchanges"],
     async (tx) => {
-      for (let i = 0; i < pcs.coinPubs.length; i++) {
-        const coin = await tx.coins.get(pcs.coinPubs[i]);
+      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");
         }
@@ -1522,7 +1525,7 @@ export async function 
getCounterpartyEffectiveDepositAmount(
         if (!denom) {
           throw Error("can't find denomination to calculate deposit amount");
         }
-        amt.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
+        amt.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
         fees.push(Amounts.parseOrThrow(denom.feeDeposit));
         exchangeSet.add(coin.exchangeBaseUrl);
       }
@@ -1574,8 +1577,8 @@ async function getTotalFeesForDepositAmount(
   await wex.db.runReadOnlyTx(
     ["coins", "denominations", "exchanges", "exchangeDetails"],
     async (tx) => {
-      for (let i = 0; i < pcs.coinPubs.length; i++) {
-        const coin = await tx.coins.get(pcs.coinPubs[i]);
+      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");
         }
@@ -1599,7 +1602,7 @@ async function getTotalFeesForDepositAmount(
         );
         const amountLeft = Amounts.sub(
           denom.value,
-          pcs.coinContributions[i],
+          pcs.coins[i].contribution,
         ).amount;
         const refreshCost = getTotalRefreshCost(
           allDenoms,
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts 
b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
index 3b618f797..03e702568 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts
@@ -22,11 +22,11 @@ import {
   TransactionAmountMode,
 } from "@gnu-taler/taler-util";
 import test, { ExecutionContext } from "ava";
-import { CoinInfo } from "./coinSelection.js";
 import {
+  CoinInfo,
   convertDepositAmountForAvailableCoins,
-  getMaxDepositAmountForAvailableCoins,
   convertWithdrawalAmountFromAvailableCoins,
+  getMaxDepositAmountForAvailableCoins,
 } from "./instructedAmountConversion.js";
 
 function makeCurrencyHelper(currency: string) {
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts 
b/packages/taler-wallet-core/src/instructedAmountConversion.ts
index ccad050bf..63ccb8b56 100644
--- a/packages/taler-wallet-core/src/instructedAmountConversion.ts
+++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -31,10 +31,23 @@ import {
   parsePaytoUri,
   strcmp,
 } from "@gnu-taler/taler-util";
-import { CoinInfo } from "./coinSelection.js";
 import { DenominationRecord, timestampProtocolFromDb } from "./db.js";
 import { getExchangeWireDetailsInTx } from "./exchanges.js";
-import { InternalWalletState, WalletExecutionContext } from "./wallet.js";
+import { WalletExecutionContext } from "./wallet.js";
+
+export interface CoinInfo {
+  id: string;
+  value: AmountJson;
+  denomDeposit: AmountJson;
+  denomWithdraw: AmountJson;
+  denomRefresh: AmountJson;
+  totalAvailable: number | undefined;
+  exchangeWire: AmountJson | undefined;
+  exchangePurse: AmountJson | undefined;
+  duration: Duration;
+  exchangeBaseUrl: string;
+  maxAge: number;
+}
 
 /**
  * If the operation going to be plan subtracts
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts 
b/packages/taler-wallet-core/src/pay-merchant.ts
index ed58dc404..a155d6298 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -111,6 +111,7 @@ import {
 import { EddsaKeypair } from "./crypto/cryptoImplementation.js";
 import {
   CoinRecord,
+  DbCoinSelection,
   DenominationRecord,
   PurchaseRecord,
   PurchaseStatus,
@@ -445,11 +446,11 @@ export async function getTotalPaymentCost(
   wex: WalletExecutionContext,
   pcs: PayCoinSelection,
 ): Promise<AmountJson> {
-  const currency = Amounts.currencyOf(pcs.paymentAmount);
+  const currency = Amounts.currencyOf(pcs.customerDepositFees);
   return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
     const costs: AmountJson[] = [];
-    for (let i = 0; i < pcs.coinPubs.length; i++) {
-      const coin = await tx.coins.get(pcs.coinPubs[i]);
+    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");
       }
@@ -470,7 +471,7 @@ export async function getTotalPaymentCost(
       );
       const amountLeft = Amounts.sub(
         denom.value,
-        pcs.coinContributions[i],
+        pcs.coins[i].contribution,
       ).amount;
       const refreshCost = getTotalRefreshCost(
         allDenoms,
@@ -478,10 +479,10 @@ export async function getTotalPaymentCost(
         amountLeft,
         wex.ws.config.testing.denomselAllowLate,
       );
-      costs.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
+      costs.push(Amounts.parseOrThrow(pcs.coins[i].contribution));
       costs.push(refreshCost);
     }
-    const zero = Amounts.zeroOfAmount(pcs.paymentAmount);
+    const zero = Amounts.zeroOfAmount(pcs.customerDepositFees);
     return Amounts.sum([zero, ...costs]).amount;
   });
 }
@@ -617,7 +618,8 @@ async function processDownloadProposal(
 
   if (proposal.purchaseStatus != PurchaseStatus.PendingDownloadingProposal) {
     logger.error(
-      `unexpected state 
${proposal.purchaseStatus}/${PurchaseStatus[proposal.purchaseStatus]
+      `unexpected state ${proposal.purchaseStatus}/${
+        PurchaseStatus[proposal.purchaseStatus]
       } for ${ctx.transactionId} in processDownloadProposal`,
     );
     return TaskRunResult.finished();
@@ -873,7 +875,8 @@ async function createOrReusePurchase(
     oldProposal.claimToken === claimToken
   ) {
     logger.info(
-      `Found old proposal (status=${PurchaseStatus[oldProposal.purchaseStatus]
+      `Found old proposal (status=${
+        PurchaseStatus[oldProposal.purchaseStatus]
       }) for order ${orderId} at ${merchantBaseUrl}`,
     );
     if (oldProposal.purchaseStatus === PurchaseStatus.DialogShared) {
@@ -1137,26 +1140,10 @@ async function handleInsufficientFunds(
   await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
     for (let i = 0; i < payCoinSelection.coinPubs.length; i++) {
       const coinPub = payCoinSelection.coinPubs[i];
-      if (coinPub === brokenCoinPub) {
-        continue;
-      }
       const contrib = payCoinSelection.coinContributions[i];
-      const coin = await tx.coins.get(coinPub);
-      if (!coin) {
-        continue;
-      }
-      const denom = await tx.denominations.get([
-        coin.exchangeBaseUrl,
-        coin.denomPubHash,
-      ]);
-      if (!denom) {
-        continue;
-      }
       prevPayCoins.push({
         coinPub,
         contribution: Amounts.parseOrThrow(contrib),
-        exchangeBaseUrl: coin.exchangeBaseUrl,
-        feeDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
       });
     }
   });
@@ -1199,7 +1186,11 @@ async function handleInsufficientFunds(
       if (!payInfo) {
         return;
       }
-      payInfo.payCoinSelection = res.coinSel;
+      // Convert to DB format
+      payInfo.payCoinSelection = {
+        coinContributions: res.coinSel.coins.map((x) => x.contribution),
+        coinPubs: res.coinSel.coins.map((x) => x.coinPub),
+      };
       payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
       await tx.purchases.put(p);
       await spendCoins(wex, tx, {
@@ -1286,13 +1277,14 @@ async function checkPaymentByProposalId(
     purchase.purchaseStatus === PurchaseStatus.DialogProposed ||
     purchase.purchaseStatus === PurchaseStatus.DialogShared
   ) {
+    const instructedAmount = Amounts.parseOrThrow(contractData.amount);
     // If not already paid, check if we could pay for it.
     const res = await selectPayCoins(wex, {
       restrictExchanges: {
         auditors: [],
         exchanges: contractData.allowedExchanges,
       },
-      contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
+      contractTermsAmount: instructedAmount,
       depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
       wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
       wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
@@ -1327,7 +1319,7 @@ async function checkPaymentByProposalId(
       transactionId,
       proposalId: proposal.proposalId,
       amountEffective: Amounts.stringify(totalCost),
-      amountRaw: Amounts.stringify(res.coinSel.paymentAmount),
+      amountRaw: Amounts.stringify(instructedAmount),
       contractTermsHash: d.contractData.contractTermsHash,
       talerUri,
     };
@@ -1599,7 +1591,7 @@ export async function preparePayForTemplate(
  */
 export async function generateDepositPermissions(
   wex: WalletExecutionContext,
-  payCoinSel: PayCoinSelection,
+  payCoinSel: DbCoinSelection,
   contractData: WalletContractData,
 ): Promise<CoinDepositPermission[]> {
   const depositPermissions: CoinDepositPermission[] = [];
@@ -1608,7 +1600,7 @@ export async function generateDepositPermissions(
     denom: DenominationRecord;
   }> = [];
   await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
-    for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
+    for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
       const coin = await tx.coins.get(payCoinSel.coinPubs[i]);
       if (!coin) {
         throw Error("can't pay, allocated coin not found anymore");
@@ -1626,7 +1618,7 @@ export async function generateDepositPermissions(
     }
   });
 
-  for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
+  for (let i = 0; i < payCoinSel.coinContributions.length; i++) {
     const { coin, denom } = coinWithDenom[i];
     let wireInfoHash: string;
     wireInfoHash = contractData.wireInfoHash;
@@ -1881,7 +1873,10 @@ export async function confirmPay(
         case PurchaseStatus.DialogShared:
         case PurchaseStatus.DialogProposed:
           p.payInfo = {
-            payCoinSelection: coinSelection,
+            payCoinSelection: {
+              coinContributions: coinSelection.coins.map((x) => 
x.contribution),
+              coinPubs: coinSelection.coins.map((x) => x.coinPub),
+            },
             payCoinSelectionUid: encodeCrock(getRandomBytes(16)),
             totalPayCost: Amounts.stringify(payCostInfo),
           };
@@ -1895,9 +1890,9 @@ export async function confirmPay(
               tag: TransactionType.Payment,
               proposalId: proposalId,
             }),
-            coinPubs: coinSelection.coinPubs,
-            contributions: coinSelection.coinContributions.map((x) =>
-              Amounts.parseOrThrow(x),
+            coinPubs: coinSelection.coins.map((x) => x.coinPub),
+            contributions: coinSelection.coins.map((x) =>
+              Amounts.parseOrThrow(x.contribution),
             ),
             refreshReason: RefreshReason.PayMerchant,
           });
diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts 
b/packages/taler-wallet-core/src/pay-peer-common.ts
index ff035d5e5..599010c1d 100644
--- a/packages/taler-wallet-core/src/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/pay-peer-common.ts
@@ -22,6 +22,7 @@ import {
   AmountString,
   Amounts,
   Codec,
+  SelectedCoin,
   TalerProtocolTimestamp,
   buildCodecForObject,
   checkDbInvariant,
@@ -29,9 +30,8 @@ import {
   codecForTimestamp,
   codecOptional,
 } from "@gnu-taler/taler-util";
-import type { SelectedPeerCoin } from "./coinSelection.js";
 import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
-import { PeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
+import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js";
 import { getTotalRefreshCost } from "./refresh.js";
 import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
 import { getCandidateWithdrawalDenomsTx } from "./withdraw.js";
@@ -41,7 +41,7 @@ import { getCandidateWithdrawalDenomsTx } from 
"./withdraw.js";
  */
 export async function queryCoinInfosForSelection(
   wex: WalletExecutionContext,
-  csel: PeerPushPaymentCoinSelection,
+  csel: DbPeerPushPaymentCoinSelection,
 ): Promise<SpendCoinDetails[]> {
   let infos: SpendCoinDetails[] = [];
   await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
@@ -74,7 +74,7 @@ export async function queryCoinInfosForSelection(
 
 export async function getTotalPeerPaymentCost(
   wex: WalletExecutionContext,
-  pcs: SelectedPeerCoin[],
+  pcs: SelectedCoin[],
 ): Promise<AmountJson> {
   const currency = Amounts.currencyOf(pcs[0].contribution);
   return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
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 2418f08da..0ccca82a2 100644
--- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -64,7 +64,7 @@ import {
   readSuccessResponseJsonOrThrow,
   readTalerErrorResponse,
 } from "@gnu-taler/taler-util/http";
-import { PeerCoinRepair, selectPeerCoins } from "./coinSelection.js";
+import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
 import {
   PendingTaskType,
   TaskIdStr,
@@ -358,16 +358,14 @@ async function handlePurseCreationConflict(
     throw Error("invalid state (coin selection expected)");
   }
 
-  const repair: PeerCoinRepair = {
-    coinPubs: [],
-    contribs: [],
-    exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
-  };
+  const repair: PreviousPayCoins = [];
 
   for (let i = 0; i < sel.coinPubs.length; i++) {
     if (sel.coinPubs[i] != brokenCoinPub) {
-      repair.coinPubs.push(sel.coinPubs[i]);
-      repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i]));
+      repair.push({
+        coinPub: sel.coinPubs[i],
+        contribution: Amounts.parseOrThrow(sel.contributions[i]),
+      });
     }
   }
 
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 b621b9e0e..cf4e7b619 100644
--- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -48,7 +48,7 @@ import {
   readSuccessResponseJsonOrThrow,
   readTalerErrorResponse,
 } from "@gnu-taler/taler-util/http";
-import { PeerCoinRepair, selectPeerCoins } from "./coinSelection.js";
+import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js";
 import {
   PendingTaskType,
   TaskIdStr,
@@ -391,16 +391,14 @@ async function handlePurseCreationConflict(
   const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
   const sel = peerPushInitiation.coinSel;
 
-  const repair: PeerCoinRepair = {
-    coinPubs: [],
-    contribs: [],
-    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
-  };
+  const repair: PreviousPayCoins = [];
 
   for (let i = 0; i < sel.coinPubs.length; i++) {
     if (sel.coinPubs[i] != brokenCoinPub) {
-      repair.coinPubs.push(sel.coinPubs[i]);
-      repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i]));
+      repair.push({
+        coinPub: sel.coinPubs[i],
+        contribution: Amounts.parseOrThrow(sel.contributions[i]),
+      });
     }
   }
 
diff --git a/packages/taler-wallet-core/src/testing.ts 
b/packages/taler-wallet-core/src/testing.ts
index 45a29a6e3..795f963d0 100644
--- a/packages/taler-wallet-core/src/testing.ts
+++ b/packages/taler-wallet-core/src/testing.ts
@@ -77,7 +77,7 @@ import {
 import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
 import { getRefreshesForTransaction } from "./refresh.js";
 import { getTransactionById, getTransactions } from "./transactions.js";
-import type { InternalWalletState, WalletExecutionContext } from "./wallet.js";
+import type { WalletExecutionContext } from "./wallet.js";
 import { acceptWithdrawalFromUri } from "./withdraw.js";
 
 const logger = new Logger("operations/testing.ts");
@@ -883,7 +883,11 @@ export async function testPay(
     "taler://fulfillment-success/thank+you",
   );
   logger.trace("created new order with order ID", orderResp.orderId);
-  const checkPayResp = await checkPayment(wex.http, merchant, 
orderResp.orderId);
+  const checkPayResp = await checkPayment(
+    wex.http,
+    merchant,
+    orderResp.orderId,
+  );
   const talerPayUri = checkPayResp.taler_pay_uri;
   if (!talerPayUri) {
     console.error("fatal: no taler pay URI received from backend");
@@ -908,6 +912,6 @@ export async function testPay(
   });
   checkLogicInvariant(!!purchase);
   return {
-    payCoinSelection: purchase.payInfo?.payCoinSelection!,
+    numCoins: purchase.payInfo?.payCoinSelection.coinContributions.length ?? 0,
   };
 }
diff --git 
a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx 
b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
index 8cb1c49dd..731bcfed9 100644
--- a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -22,20 +22,19 @@ import {
   PreparePayResultType,
   TranslatedString,
   parsePayUri,
-  stringifyPayUri,
 } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
 import { Amount } from "./Amount.js";
 import { Part } from "./Part.js";
 import { QR } from "./QR.js";
 import { LinkSuccess, WarningBox } from "./styled/index.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Button } from "../mui/Button.js";
-import { ButtonHandler } from "../mui/handlers.js";
-import { assertUnreachable } from "../utils/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
 
 interface Props {
   payStatus: PreparePayResult;
@@ -118,7 +117,12 @@ export function PaymentButtons({
       }
       case "fee-gap": {
         BalanceMessage = i18n.str`Balance looks like it should be enough, but 
doesn't cover all fees requested by the merchant and payment processor. Please 
ensure there is at least ${Amounts.stringifyValue(
-          payStatus.balanceDetails.feeGapEstimate,
+          Amounts.stringify(
+            Amounts.sub(
+              amount,
+              payStatus.balanceDetails.maxEffectiveSpendAmount,
+            ).amount,
+          ),
         )} ${
           amount.currency
         } more balance in your wallet or ask your merchant to cover more of 
the fees.`;
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index a3f00f164..567b5c177 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -59,7 +59,7 @@ export const NoEnoughBalanceAvailable = 
tests.createExample(BaseView, {
       balanceAgeAcceptable: "USD:9" as AmountString,
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
-      feeGapEstimate: "USD:1" as AmountString,
+      maxEffectiveSpendAmount: "USD:9.5" as AmountString,
       perExchange: {},
     },
     talerUri: "taler://pay/..",
@@ -100,7 +100,7 @@ export const NoEnoughBalanceMaterial = 
tests.createExample(BaseView, {
       balanceAgeAcceptable: "USD:9" as AmountString,
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:0" as AmountString,
-      feeGapEstimate: "USD:1" as AmountString,
+      maxEffectiveSpendAmount: "USD:9.5" as AmountString,
       perExchange: {},
     },
     talerUri: "taler://pay/..",
@@ -141,7 +141,7 @@ export const NoEnoughBalanceAgeAcceptable = 
tests.createExample(BaseView, {
       balanceAgeAcceptable: "USD:9" as AmountString,
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
-      feeGapEstimate: "USD:1" as AmountString,
+      maxEffectiveSpendAmount: "USD:9.5" as AmountString,
       perExchange: {},
     },
     talerUri: "taler://pay/..",
@@ -183,7 +183,7 @@ export const NoEnoughBalanceMerchantAcceptable = 
tests.createExample(BaseView, {
       balanceAgeAcceptable: "USD:10" as AmountString,
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
-      feeGapEstimate: "USD:1" as AmountString,
+      maxEffectiveSpendAmount: "USD:9.5" as AmountString,
       perExchange: {},
     },
     talerUri: "taler://pay/..",
@@ -226,7 +226,7 @@ export const NoEnoughBalanceMerchantDepositable = 
tests.createExample(
         balanceAgeAcceptable: "USD:10" as AmountString,
         balanceMerchantAcceptable: "USD:10" as AmountString,
         balanceMerchantDepositable: "USD:9" as AmountString,
-        feeGapEstimate: "USD:1" as AmountString,
+        maxEffectiveSpendAmount: "USD:9.5" as AmountString,
         perExchange: {},
       },
       talerUri: "taler://pay/..",
@@ -268,7 +268,7 @@ export const NoEnoughBalanceFeeGap = 
tests.createExample(BaseView, {
       balanceAgeAcceptable: "USD:10" as AmountString,
       balanceMerchantAcceptable: "USD:10" as AmountString,
       balanceMerchantDepositable: "USD:10" as AmountString,
-      feeGapEstimate: "USD:1" as AmountString,
+      maxEffectiveSpendAmount: "USD:9.5" as AmountString,
       perExchange: {},
     },
     talerUri: "taler://pay/..",
diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts 
b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
index d3cbc66a0..f092801ed 100644
--- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts
@@ -17,19 +17,18 @@
 import {
   AmountString,
   Amounts,
-  TalerError,
   TalerErrorCode,
   TalerProtocolTimestamp,
 } from "@gnu-taler/taler-util";
 import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
 import { isFuture, parse } from "date-fns";
-import { useEffect, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
 import { alertFromError, useAlertContext } from "../../context/alert.js";
 import { useBackendContext } from "../../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
 import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
-import { Props, State } from "./index.js";
 import { BackgroundError, WxApiType } from "../../wxApi.js";
+import { Props, State } from "./index.js";
 
 export function useComponentState({
   amount: amountStr,
@@ -164,11 +163,14 @@ async function checkPeerPushDebitAndCheckMax(
     const material = Amounts.parseOrThrow(
       e.errorDetail.insufficientBalanceDetails.balanceMaterial,
     );
-    const gap = Amounts.parseOrThrow(
-      e.errorDetail.insufficientBalanceDetails.feeGapEstimate,
-    );
-    const newAmount = Amounts.sub(material, gap).amount;
     const amount = Amounts.parseOrThrow(amountState);
+    const gap = Amounts.sub(
+      amount,
+      Amounts.parseOrThrow(
+        e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount,
+      ),
+    ).amount;
+    const newAmount = Amounts.sub(material, gap).amount;
     if (Amounts.cmp(newAmount, amount) === 0) {
       //insufficient balance and the exception didn't give
       //a good response that allow us to try again

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