gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: towards refactoring coin sele


From: gnunet
Subject: [taler-wallet-core] branch master updated: towards refactoring coin selection
Date: Wed, 06 Mar 2024 20:34:46 +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 91be5b89c towards refactoring coin selection
91be5b89c is described below

commit 91be5b89cd92c53d6aa2f68247f9626c8bc8f64a
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Mar 6 14:17:31 2024 +0100

    towards refactoring coin selection
---
 packages/taler-util/src/errors.ts                  |    3 +-
 packages/taler-util/src/wallet-types.ts            |   43 +-
 packages/taler-wallet-core/src/balance.ts          |  257 +++--
 .../taler-wallet-core/src/coinSelection.test.ts    |  101 +-
 packages/taler-wallet-core/src/coinSelection.ts    | 1202 ++++++++++----------
 packages/taler-wallet-core/src/deposits.ts         |   22 +-
 packages/taler-wallet-core/src/pay-merchant.ts     |   32 +-
 .../src/cta/Payment/stories.tsx                    |    6 +
 8 files changed, 827 insertions(+), 839 deletions(-)

diff --git a/packages/taler-util/src/errors.ts 
b/packages/taler-util/src/errors.ts
index 155562865..c4733a194 100644
--- a/packages/taler-util/src/errors.ts
+++ b/packages/taler-util/src/errors.ts
@@ -26,7 +26,6 @@
 import {
   AbsoluteTime,
   PayMerchantInsufficientBalanceDetails,
-  PayPeerInsufficientBalanceDetails,
   TalerErrorCode,
   TalerErrorDetail,
   TransactionType,
@@ -136,7 +135,7 @@ export interface DetailsMap {
     insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
   };
   [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
-    insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+    insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
   };
   [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
     numErrors: number;
diff --git a/packages/taler-util/src/wallet-types.ts 
b/packages/taler-util/src/wallet-types.ts
index 1efa6651f..9fe114b3d 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -877,6 +877,14 @@ export interface PayMerchantInsufficientBalanceDetails {
    * on the coin selection for the amount that would be additionally withdrawn.
    */
   feeGapEstimate: AmountString;
+
+  perExchange: {
+    [url: string]: {
+      balanceAvailable: AmountString;
+      balanceMaterial: AmountString;
+      feeGapEstimate: AmountString;
+    };
+  };
 }
 
 export const codecForPayMerchantInsufficientBalanceDetails =
@@ -889,6 +897,7 @@ export const codecForPayMerchantInsufficientBalanceDetails =
       .property("balanceMerchantAcceptable", codecForAmountString())
       .property("balanceMerchantDepositable", codecForAmountString())
       .property("feeGapEstimate", codecForAmountString())
+      .property("perExchange", codecForAny())
       .build("PayMerchantInsufficientBalanceDetails");
 
 export const codecForPreparePayResultInsufficientBalance =
@@ -2858,40 +2867,6 @@ export interface InitiatePeerPullCreditResponse {
   transactionId: TransactionIdStr;
 }
 
-/**
- * Detailed reason for why the wallet's balance is insufficient.
- */
-export interface PayPeerInsufficientBalanceDetails {
-  /**
-   * Amount requested by the merchant.
-   */
-  amountRequested: AmountString;
-
-  /**
-   * Balance of type "available" (see balance.ts for definition).
-   */
-  balanceAvailable: AmountString;
-
-  /**
-   * Balance of type "material" (see balance.ts for definition).
-   */
-  balanceMaterial: AmountString;
-
-  /**
-   * If non-zero, the largest fee gap estimate of an exchange
-   * where we would otherwise have enough balance available.
-   */
-  feeGapEstimate: AmountString;
-
-  perExchange: {
-    [url: string]: {
-      balanceAvailable: AmountString;
-      balanceMaterial: AmountString;
-      feeGapEstimate: AmountString;
-    };
-  };
-}
-
 export interface ValidateIbanRequest {
   iban: string;
 }
diff --git a/packages/taler-wallet-core/src/balance.ts 
b/packages/taler-wallet-core/src/balance.ts
index 6dc0783c0..a77358363 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -468,12 +468,16 @@ export interface MerchantPaymentBalanceDetails {
   balanceAvailable: AmountJson;
 }
 
-export interface MerchantPaymentRestrictionsForBalance {
+export interface PaymentRestrictionsForBalance {
   currency: string;
   minAge: number;
-  acceptedExchanges: AllowedExchangeInfo[];
-  acceptedAuditors: AllowedAuditorInfo[];
-  acceptedWireMethods: string[];
+  restrictExchanges:
+    | {
+        exchanges: AllowedExchangeInfo[];
+        auditors: AllowedAuditorInfo[];
+      }
+    | undefined;
+  restrictWireMethods: string[] | undefined;
 }
 
 export interface AcceptableExchanges {
@@ -492,69 +496,73 @@ export interface AcceptableExchanges {
 /**
  * Get all exchanges that are acceptable for a particular payment.
  */
-export async function getAcceptableExchangeBaseUrls(
+async function getAcceptableExchangeBaseUrlsInTx(
   wex: WalletExecutionContext,
-  req: MerchantPaymentRestrictionsForBalance,
+  tx: WalletDbReadOnlyTransaction<["exchanges", "exchangeDetails"]>,
+  req: PaymentRestrictionsForBalance,
 ): Promise<AcceptableExchanges> {
   const acceptableExchangeUrls = new Set<string>();
   const depositableExchangeUrls = new Set<string>();
-  await wex.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => {
-    // FIXME: We should have a DB index to look up all exchanges
-    // for a particular auditor ...
+  // 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>();
+  const canonExchanges = new Set<string>();
+  const canonAuditors = new Set<string>();
 
-    for (const exchangeHandle of req.acceptedExchanges) {
+  if (req.restrictExchanges) {
+    for (const exchangeHandle of req.restrictExchanges.exchanges) {
       const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
       canonExchanges.add(normUrl);
     }
 
-    for (const auditorHandle of req.acceptedAuditors) {
+    for (const auditorHandle of req.restrictExchanges.auditors) {
       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;
-      }
+  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;
+    let acceptable = false;
 
-      if (canonExchanges.has(exchange.baseUrl)) {
+    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;
       }
-      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?
+    if (!acceptable) {
+      return;
+    }
+    // FIXME: Also consider exchange and auditor public key
+    // instead of just base URLs?
+
+    let wireMethodSupported = false;
 
-      let wireMethodSupported = false;
+    if (req.restrictWireMethods) {
       for (const acc of exchangeDetails.wireInfo.accounts) {
         const pp = parsePaytoUri(acc.payto_uri);
         checkLogicInvariant(!!pp);
-        for (const wm of req.acceptedWireMethods) {
+        for (const wm of req.restrictWireMethods) {
           if (pp.targetType === wm) {
             wireMethodSupported = true;
             break;
@@ -564,12 +572,14 @@ export async function getAcceptableExchangeBaseUrls(
           }
         }
       }
+    } else {
+      wireMethodSupported = true;
+    }
 
-      acceptableExchangeUrls.add(exchange.baseUrl);
-      if (wireMethodSupported) {
-        depositableExchangeUrls.add(exchange.baseUrl);
-      }
-    });
+    acceptableExchangeUrls.add(exchange.baseUrl);
+    if (wireMethodSupported) {
+      depositableExchangeUrls.add(exchange.baseUrl);
+    }
   });
   return {
     acceptableExchanges: [...acceptableExchangeUrls],
@@ -606,9 +616,24 @@ export interface MerchantPaymentBalanceDetails {
 
 export async function getMerchantPaymentBalanceDetails(
   wex: WalletExecutionContext,
-  req: MerchantPaymentRestrictionsForBalance,
+  req: PaymentRestrictionsForBalance,
 ): Promise<MerchantPaymentBalanceDetails> {
-  const acceptability = await getAcceptableExchangeBaseUrls(wex, req);
+  return await wex.db.runReadOnlyTx(
+    ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"],
+    async (tx) => {
+      return getMerchantPaymentBalanceDetailsInTx(wex, tx, req);
+    },
+  );
+}
+
+export async function getMerchantPaymentBalanceDetailsInTx(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<
+    ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"]
+  >,
+  req: PaymentRestrictionsForBalance,
+): Promise<MerchantPaymentBalanceDetails> {
+  const acceptability = await getAcceptableExchangeBaseUrlsInTx(wex, tx, req);
 
   const d: MerchantPaymentBalanceDetails = {
     balanceAvailable: Amounts.zeroOfCurrency(req.currency),
@@ -618,53 +643,46 @@ export async function getMerchantPaymentBalanceDetails(
     balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
   };
 
-  await wex.db.runReadOnlyTx(
-    ["coinAvailability", "refreshGroups"],
-    async (tx) => {
-      await tx.coinAvailability.iter().forEach((ca) => {
-        if (ca.currency != req.currency) {
-          return;
-        }
-        const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
-        const coinAmount: AmountJson = Amounts.mult(
-          singleCoinAmount,
-          ca.freshCoinCount,
+  await tx.coinAvailability.iter().forEach((ca) => {
+    if (ca.currency != req.currency) {
+      return;
+    }
+    const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+    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;
-        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,
+        if (acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)) {
+          d.balanceMerchantDepositable = Amounts.add(
+            d.balanceMerchantDepositable,
             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) => {
-        if (r.currency != req.currency) {
-          return;
-        }
-        d.balanceAvailable = Amounts.add(
-          d.balanceAvailable,
-          computeRefreshGroupAvailableAmount(r),
-        ).amount;
-      });
-    },
-  );
+  await tx.refreshGroups.iter().forEach((r) => {
+    if (r.currency != req.currency) {
+      return;
+    }
+    d.balanceAvailable = Amounts.add(
+      d.balanceAvailable,
+      computeRefreshGroupAvailableAmount(r),
+    ).amount;
+  });
 
   return d;
 }
@@ -697,9 +715,11 @@ export async function getBalanceDetail(
 
   return await getMerchantPaymentBalanceDetails(wex, {
     currency: req.currency,
-    acceptedAuditors: [],
-    acceptedExchanges: exchanges,
-    acceptedWireMethods: wires,
+    restrictExchanges: {
+      auditors: [],
+      exchanges,
+    },
+    restrictWireMethods: wires,
     minAge: 0,
   });
 }
@@ -763,3 +783,50 @@ export async function getPeerPaymentBalanceDetailsInTx(
     balanceMaterial,
   };
 }
+
+/**
+ * Get information about the balance at a given exchange
+ * with certain restrictions.
+ */
+export async function getExchangePaymentBalanceDetailsInTx(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<["coinAvailability", "refreshGroups"]>,
+  req: PeerPaymentRestrictionsForBalance,
+): Promise<PeerPaymentBalanceDetails> {
+  let balanceAvailable = Amounts.zeroOfCurrency(req.currency);
+  let balanceMaterial = Amounts.zeroOfCurrency(req.currency);
+
+  await tx.coinAvailability.iter().forEach((ca) => {
+    if (ca.currency != req.currency) {
+      return;
+    }
+    if (
+      req.restrictExchangeTo &&
+      req.restrictExchangeTo !== ca.exchangeBaseUrl
+    ) {
+      return;
+    }
+    const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+    const coinAmount: AmountJson = Amounts.mult(
+      singleCoinAmount,
+      ca.freshCoinCount,
+    ).amount;
+    balanceAvailable = Amounts.add(balanceAvailable, coinAmount).amount;
+    balanceMaterial = Amounts.add(balanceMaterial, coinAmount).amount;
+  });
+
+  await tx.refreshGroups.iter().forEach((r) => {
+    if (r.currency != req.currency) {
+      return;
+    }
+    balanceAvailable = Amounts.add(
+      balanceAvailable,
+      computeRefreshGroupAvailableAmount(r),
+    ).amount;
+  });
+
+  return {
+    balanceAvailable,
+    balanceMaterial,
+  };
+}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts 
b/packages/taler-wallet-core/src/coinSelection.test.ts
index 4fac244fc..6eae9deaa 100644
--- a/packages/taler-wallet-core/src/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -24,8 +24,8 @@ import {
 import test from "ava";
 import {
   AvailableDenom,
-  PeerCoinSelectionTally,
-  testing_greedySelectPeer,
+  CoinSelectionTally,
+  emptyTallyForPeerPayment,
   testing_selectGreedy,
 } from "./coinSelection.js";
 
@@ -42,12 +42,13 @@ const inThePast = AbsoluteTime.toProtocolTimestamp(
 
 test("p2p: should select the coin", (t) => {
   const instructedAmount = Amounts.parseOrThrow("LOCAL:2");
-  const tally = {
-    amountRemaining: instructedAmount,
-    depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
-    lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
-  } satisfies PeerCoinSelectionTally;
-  const coins = testing_greedySelectPeer(
+  const tally = emptyTallyForPeerPayment(instructedAmount);
+  t.log(`tally before: ${j2s(tally)}`);
+  const coins = testing_selectGreedy(
+    {
+      wireFeeAmortization: 1,
+      wireFeesPerExchange: {},
+    },
     createCandidates([
       {
         amount: "LOCAL:10" as AmountString,
@@ -59,7 +60,8 @@ test("p2p: should select the coin", (t) => {
     tally,
   );
 
-  t.log(j2s(coins));
+  t.log(`coins: ${j2s(coins)}`);
+  t.log(`tally: ${j2s(tally)}`);
 
   t.assert(coins != null);
 
@@ -73,22 +75,16 @@ test("p2p: should select the coin", (t) => {
       expireWithdraw: inTheDistantFuture,
     },
   });
-
-  t.deepEqual(tally, {
-    amountRemaining: Amounts.parseOrThrow("LOCAL:0"),
-    depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"),
-    lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
-  });
 });
 
 test("p2p: should select 3 coins", (t) => {
   const instructedAmount = Amounts.parseOrThrow("LOCAL:20");
-  const tally = {
-    amountRemaining: instructedAmount,
-    depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
-    lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
-  } satisfies PeerCoinSelectionTally;
-  const coins = testing_greedySelectPeer(
+  const tally = emptyTallyForPeerPayment(instructedAmount);
+  const coins = testing_selectGreedy(
+    {
+      wireFeeAmortization: 1,
+      wireFeesPerExchange: {},
+    },
     createCandidates([
       {
         amount: "LOCAL:10" as AmountString,
@@ -114,22 +110,16 @@ test("p2p: should select 3 coins", (t) => {
       expireWithdraw: inTheDistantFuture,
     },
   });
-
-  t.deepEqual(tally, {
-    amountRemaining: Amounts.parseOrThrow("LOCAL:0"),
-    depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"),
-    lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
-  });
 });
 
 test("p2p: can't select since the instructed amount is too high", (t) => {
   const instructedAmount = Amounts.parseOrThrow("LOCAL:60");
-  const tally = {
-    amountRemaining: instructedAmount,
-    depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
-    lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
-  } satisfies PeerCoinSelectionTally;
-  const coins = testing_greedySelectPeer(
+  const tally = emptyTallyForPeerPayment(instructedAmount);
+  const coins = testing_selectGreedy(
+    {
+      wireFeeAmortization: 1,
+      wireFeesPerExchange: {},
+    },
     createCandidates([
       {
         amount: "LOCAL:10" as AmountString,
@@ -142,12 +132,6 @@ test("p2p: can't select since the instructed amount is too 
high", (t) => {
   );
 
   t.is(coins, undefined);
-
-  t.deepEqual(tally, {
-    amountRemaining: Amounts.parseOrThrow("LOCAL:10.5"),
-    depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"),
-    lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
-  });
 });
 
 test("pay: select one coin to pay with fee", (t) => {
@@ -162,22 +146,11 @@ test("pay: select one coin to pay with fee", (t) => {
     customerWireFees: zero,
     wireFeeCoveredForExchange: new Set<string>(),
     lastDepositFee: zero,
-  };
+  } satisfies CoinSelectionTally;
   const coins = testing_selectGreedy(
     {
-      auditors: [],
-      exchanges: [
-        {
-          exchangeBaseUrl: "http://exchange.localhost/";,
-          exchangePub: "E5M8CGRDHXF1RCVP3B8TQCTDYNQ7T4XHWR5SVEQRGVVMVME41VJ0",
-        },
-      ],
-      contractTermsAmount: payment,
-      depositFeeLimit: zero,
       wireFeeAmortization: 1,
-      wireFeeLimit: zero,
-      prevPayCoins: [],
-      wireMethod: "x-taler-bank",
+      wireFeesPerExchange: { "http://exchange.localhost/": exchangeWireFee },
     },
     createCandidates([
       {
@@ -187,7 +160,6 @@ test("pay: select one coin to pay with fee", (t) => {
         fromExchange: "http://exchange.localhost/";,
       },
     ]),
-    { "http://exchange.localhost/": exchangeWireFee },
     tally,
   );
 
@@ -203,13 +175,13 @@ test("pay: select one coin to pay with fee", (t) => {
   });
 
   t.deepEqual(tally, {
-    amountPayRemaining: Amounts.parseOrThrow("LOCAL:2"),
+    amountPayRemaining: Amounts.parseOrThrow("LOCAL:0"),
     amountWireFeeLimitRemaining: zero,
     amountDepositFeeLimitRemaining: zero,
-    customerDepositFees: zero,
+    customerDepositFees: Amounts.parse("LOCAL:0.1"),
     customerWireFees: zero,
-    wireFeeCoveredForExchange: new Set(),
-    lastDepositFee: zero,
+    wireFeeCoveredForExchange: new Set(["http://exchange.localhost/";]),
+    lastDepositFee: Amounts.parse("LOCAL:0.1"),
   });
 });
 
@@ -309,11 +281,14 @@ test("p2p: regression STATER", (t) => {
     },
   ];
   const instructedAmount = Amounts.parseOrThrow("STATER:1");
-  const tally = {
-    amountRemaining: instructedAmount,
-    depositFeesAcc: Amounts.parseOrThrow("STATER:0"),
-    lastDepositFee: Amounts.parseOrThrow("STATER:0"),
-  } satisfies PeerCoinSelectionTally;
-  const res = testing_greedySelectPeer(candidates as any, tally);
+  const tally = emptyTallyForPeerPayment(instructedAmount);
+  const res = testing_selectGreedy(
+    {
+      wireFeeAmortization: 1,
+      wireFeesPerExchange: {},
+    },
+    candidates as any,
+    tally,
+  );
   t.assert(!!res);
 });
diff --git a/packages/taler-wallet-core/src/coinSelection.ts 
b/packages/taler-wallet-core/src/coinSelection.ts
index 3ece5546c..c44ca3d17 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -39,7 +39,6 @@ import {
   CoinPublicKeyString,
   CoinStatus,
   DenominationInfo,
-  DenominationPubKey,
   DenomSelectionState,
   Duration,
   ForcedCoinSel,
@@ -50,62 +49,25 @@ import {
   parsePaytoUri,
   PayCoinSelection,
   PayMerchantInsufficientBalanceDetails,
-  PayPeerInsufficientBalanceDetails,
   strcmp,
   TalerProtocolTimestamp,
   UnblindedSignature,
 } from "@gnu-taler/taler-util";
 import {
-  getMerchantPaymentBalanceDetails,
-  getPeerPaymentBalanceDetailsInTx,
+  getExchangePaymentBalanceDetailsInTx,
+  getMerchantPaymentBalanceDetailsInTx,
 } from "./balance.js";
 import { getAutoRefreshExecuteThreshold } from "./common.js";
 import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js";
 import { isWithdrawableDenom } from "./denominations.js";
-import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import {
+  ExchangeWireDetails,
+  getExchangeWireDetailsInTx,
+} from "./exchanges.js";
 import { getDenomInfo, WalletExecutionContext } from "./wallet.js";
 
 const logger = new Logger("coinSelection.ts");
 
-/**
- * Structure to describe a coin that is available to be
- * used in a payment.
- */
-export interface AvailableCoinInfo {
-  /**
-   * Public key of the coin.
-   */
-  coinPub: string;
-
-  /**
-   * Coin's denomination public key.
-   *
-   * FIXME: We should only need the denomPubHash here, if at all.
-   */
-  denomPub: DenominationPubKey;
-
-  /**
-   * Full value of the coin.
-   */
-  value: AmountJson;
-
-  /**
-   * Amount still remaining (typically the full amount,
-   * as coins are always refreshed after use.)
-   */
-  availableAmount: AmountJson;
-
-  /**
-   * Deposit fee for the coin.
-   */
-  feeDeposit: AmountJson;
-
-  exchangeBaseUrl: string;
-
-  maxAge: number;
-  ageCommitmentProof?: AgeCommitmentProof;
-}
-
 export type PreviousPayCoins = {
   coinPub: string;
   contribution: AmountJson;
@@ -113,19 +75,9 @@ export type PreviousPayCoins = {
   exchangeBaseUrl: string;
 }[];
 
-export interface CoinCandidateSelection {
-  candidateCoins: AvailableCoinInfo[];
-  wireFeesPerExchange: Record<string, AmountJson>;
-}
-
-export interface SelectPayCoinRequest {
-  candidates: CoinCandidateSelection;
-  contractTermsAmount: AmountJson;
-  depositFeeLimit: AmountJson;
-  wireFeeLimit: AmountJson;
-  wireFeeAmortization: number;
-  prevPayCoins?: PreviousPayCoins;
-  requiredMinimumAge?: number;
+export interface ExchangeRestrictionSpec {
+  exchanges: AllowedExchangeInfo[];
+  auditors: AllowedAuditorInfo[];
 }
 
 export interface CoinSelectionTally {
@@ -159,26 +111,20 @@ export interface CoinSelectionTally {
  * Account for the fees of spending a coin.
  */
 function tallyFees(
-  tally: Readonly<CoinSelectionTally>,
+  tally: CoinSelectionTally,
   wireFeesPerExchange: Record<string, AmountJson>,
   wireFeeAmortization: number,
   exchangeBaseUrl: string,
   feeDeposit: AmountJson,
-): CoinSelectionTally {
+): void {
   const currency = tally.amountPayRemaining.currency;
-  let amountWireFeeLimitRemaining = tally.amountWireFeeLimitRemaining;
-  let amountDepositFeeLimitRemaining = tally.amountDepositFeeLimitRemaining;
-  let customerDepositFees = tally.customerDepositFees;
-  let customerWireFees = tally.customerWireFees;
-  let amountPayRemaining = tally.amountPayRemaining;
-  const wireFeeCoveredForExchange = new Set(tally.wireFeeCoveredForExchange);
 
   if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) {
     const wf =
       wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.zeroOfCurrency(currency);
-    const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf);
-    amountWireFeeLimitRemaining = Amounts.sub(
-      amountWireFeeLimitRemaining,
+    const wfForgiven = Amounts.min(tally.amountWireFeeLimitRemaining, wf);
+    tally.amountWireFeeLimitRemaining = Amounts.sub(
+      tally.amountWireFeeLimitRemaining,
       wfForgiven,
     ).amount;
     // The remaining, amortized amount needs to be paid by the
@@ -187,45 +133,48 @@ function tallyFees(
       Amounts.sub(wf, wfForgiven).amount,
       wireFeeAmortization,
     );
-
     // This is the amount forgiven via the deposit fee allowance.
     const wfDepositForgiven = Amounts.min(
-      amountDepositFeeLimitRemaining,
+      tally.amountDepositFeeLimitRemaining,
       wfRemaining,
     );
-    amountDepositFeeLimitRemaining = Amounts.sub(
-      amountDepositFeeLimitRemaining,
+    tally.amountDepositFeeLimitRemaining = Amounts.sub(
+      tally.amountDepositFeeLimitRemaining,
       wfDepositForgiven,
     ).amount;
-
     wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
-    customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
-    amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
-    wireFeeCoveredForExchange.add(exchangeBaseUrl);
+    tally.customerWireFees = Amounts.add(
+      tally.customerWireFees,
+      wfRemaining,
+    ).amount;
+    tally.amountPayRemaining = Amounts.add(
+      tally.amountPayRemaining,
+      wfRemaining,
+    ).amount;
+    tally.wireFeeCoveredForExchange.add(exchangeBaseUrl);
   }
 
-  const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
+  const dfForgiven = Amounts.min(
+    feeDeposit,
+    tally.amountDepositFeeLimitRemaining,
+  );
 
-  amountDepositFeeLimitRemaining = Amounts.sub(
-    amountDepositFeeLimitRemaining,
+  tally.amountDepositFeeLimitRemaining = Amounts.sub(
+    tally.amountDepositFeeLimitRemaining,
     dfForgiven,
   ).amount;
 
   // How much does the user spend on deposit fees for this coin?
   const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount;
-  customerDepositFees = Amounts.add(customerDepositFees, dfRemaining).amount;
-  amountPayRemaining = Amounts.add(amountPayRemaining, dfRemaining).amount;
-
-  return {
-    amountDepositFeeLimitRemaining,
-    amountPayRemaining,
-    amountWireFeeLimitRemaining,
-    customerDepositFees,
-    customerWireFees,
-    wireFeeCoveredForExchange,
-    lastDepositFee: feeDeposit,
-  };
+  tally.customerDepositFees = Amounts.add(
+    tally.customerDepositFees,
+    dfRemaining,
+  ).amount;
+  tally.amountPayRemaining = Amounts.add(
+    tally.amountPayRemaining,
+    dfRemaining,
+  ).amount;
+  tally.lastDepositFee = feeDeposit;
 }
 
 export type SelectPayCoinsResult =
@@ -236,16 +185,13 @@ export type SelectPayCoinsResult =
   | { type: "success"; coinSel: PayCoinSelection };
 
 /**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
+ * Select coins to spend under the merchant's constraints.
  *
  * The prevPayCoins can be specified to "repair" a coin selection
  * by adding additional coins, after a broken (e.g. double-spent) coin
  * has been removed from the selection.
- *
- * This function is only exported for the sake of unit tests.
  */
-export async function selectPayCoinsNew(
+export async function selectPayCoins(
   wex: WalletExecutionContext,
   req: SelectPayCoinRequestNg,
 ): Promise<SelectPayCoinsResult> {
@@ -256,141 +202,203 @@ export async function selectPayCoinsNew(
     wireFeeAmortization,
   } = req;
 
-  // FIXME: Why don't we do this in a transaction?
-  const [candidateDenoms, wireFeesPerExchange] =
-    await selectPayMerchantCandidates(wex, req);
+  return await wex.db.runReadOnlyTx(
+    [
+      "coinAvailability",
+      "denominations",
+      "refreshGroups",
+      "exchanges",
+      "exchangeDetails",
+      "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 coinPubs: string[] = [];
-  const coinContributions: AmountJson[] = [];
-  const currency = contractTermsAmount.currency;
+      const coinPubs: string[] = [];
+      const coinContributions: AmountJson[] = [];
+      const currency = contractTermsAmount.currency;
+
+      let tally: CoinSelectionTally = {
+        amountPayRemaining: contractTermsAmount,
+        amountWireFeeLimitRemaining: wireFeeLimit,
+        amountDepositFeeLimitRemaining: depositFeeLimit,
+        customerDepositFees: Amounts.zeroOfCurrency(currency),
+        customerWireFees: Amounts.zeroOfCurrency(currency),
+        wireFeeCoveredForExchange: new Set(),
+        lastDepositFee: Amounts.zeroOfCurrency(currency),
+      };
 
-  let tally: CoinSelectionTally = {
-    amountPayRemaining: contractTermsAmount,
-    amountWireFeeLimitRemaining: wireFeeLimit,
-    amountDepositFeeLimitRemaining: depositFeeLimit,
-    customerDepositFees: Amounts.zeroOfCurrency(currency),
-    customerWireFees: Amounts.zeroOfCurrency(currency),
-    wireFeeCoveredForExchange: new Set(),
-    lastDepositFee: Amounts.zeroOfCurrency(currency),
-  };
+      const prevPayCoins = req.prevPayCoins ?? [];
 
-  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;
 
-  // Look at existing pay coin selection and tally up
-  for (const prev of prevPayCoins) {
-    tally = 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);
+      }
 
-    coinPubs.push(prev.coinPub);
-    coinContributions.push(prev.contribution);
-  }
+      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,
+        );
+      }
 
-  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(
-      req,
-      candidateDenoms,
-      wireFeesPerExchange,
-      tally,
-    );
-  }
+      if (!selectedDenom) {
+        return {
+          type: "failure",
+          insufficientBalanceDetails: await reportInsufficientBalanceDetails(
+            wex,
+            tx,
+            {
+              restrictExchanges: req.restrictExchanges,
+              instructedAmount: req.contractTermsAmount,
+              requiredMinimumAge: req.requiredMinimumAge,
+              wireMethod: req.restrictWireMethod,
+            },
+          ),
+        } satisfies SelectPayCoinsResult;
+      }
 
-  if (!selectedDenom) {
-    const details = await getMerchantPaymentBalanceDetails(wex, {
-      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);
+      const finalSel = selectedDenom;
+
+      logger.trace(`coin selection request ${j2s(req)}`);
+      logger.trace(`selected coins (via denoms) for payment: 
${j2s(finalSel)}`);
+
+      for (const dph of Object.keys(finalSel)) {
+        const selInfo = finalSel[dph];
+        const numRequested = selInfo.contributions.length;
+        const query = [
+          selInfo.exchangeBaseUrl,
+          selInfo.denomPubHash,
+          selInfo.maxAge,
+          CoinStatus.Fresh,
+        ];
+        logger.trace(`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})`,
+          );
+        }
+        coinPubs.push(...coins.map((x) => x.coinPub));
+        coinContributions.push(...selInfo.contributions);
+      }
+
+      return {
+        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),
+        },
+      };
+    },
+  );
+}
+
+interface ReportInsufficientBalanceRequest {
+  instructedAmount: AmountJson;
+  requiredMinimumAge: number | undefined;
+  restrictExchanges: ExchangeRestrictionSpec | undefined;
+  wireMethod: string | undefined;
+}
+
+export async function reportInsufficientBalanceDetails(
+  wex: WalletExecutionContext,
+  tx: WalletDbReadOnlyTransaction<
+    ["coinAvailability", "exchanges", "exchangeDetails", "refreshGroups"]
+  >,
+  req: ReportInsufficientBalanceRequest,
+): Promise<PayMerchantInsufficientBalanceDetails> {
+  const currency = Amounts.currencyOf(req.instructedAmount);
+  const details = await getMerchantPaymentBalanceDetailsInTx(wex, tx, {
+    restrictExchanges: req.restrictExchanges,
+    restrictWireMethods: req.wireMethod ? [req.wireMethod] : [],
+    currency: Amounts.currencyOf(req.instructedAmount),
+    minAge: req.requiredMinimumAge ?? 0,
+  });
+  let feeGapEstimate: AmountJson;
+
+  // FIXME: need fee gap estimate
+  // FIXME: We can probably give a better estimate.
+  // feeGapEstimate = Amounts.add(
+  //   tally.amountPayRemaining,
+  //   tally.lastDepositFee,
+  // ).amount;
+
+  feeGapEstimate = Amounts.zeroOfAmount(req.instructedAmount);
+
+  const perExchange: PayMerchantInsufficientBalanceDetails["perExchange"] = {};
+
+  const exchanges = await tx.exchanges.iter().toArray();
+
+  for (const exch of exchanges) {
+    if (exch.detailsPointer?.currency !== currency) {
+      continue;
     }
-    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 infoExchange = await getExchangePaymentBalanceDetailsInTx(wex, tx, {
+      currency,
+      restrictExchangeTo: exch.baseUrl,
+    });
+    perExchange[exch.baseUrl] = {
+      balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
+      balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
+      feeGapEstimate: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
     };
   }
 
-  const finalSel = selectedDenom;
-
-  logger.trace(`coin selection request ${j2s(req)}`);
-  logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
-
-  await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
-    for (const dph of Object.keys(finalSel)) {
-      const selInfo = finalSel[dph];
-      const numRequested = selInfo.contributions.length;
-      const query = [
-        selInfo.exchangeBaseUrl,
-        selInfo.denomPubHash,
-        selInfo.maxAge,
-        CoinStatus.Fresh,
-      ];
-      logger.trace(`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})`,
-        );
-      }
-      coinPubs.push(...coins.map((x) => x.coinPub));
-      coinContributions.push(...selInfo.contributions);
-    }
-  });
-
   return {
-    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),
-    },
+    amountRequested: Amounts.stringify(req.instructedAmount),
+    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),
+    perExchange,
   };
 }
 
@@ -426,10 +434,14 @@ export function testing_selectGreedy(
   return selectGreedy(...args);
 }
 
+export interface SelectGreedyRequest {
+  wireFeeAmortization: number;
+  wireFeesPerExchange: Record<string, AmountJson>;
+}
+
 function selectGreedy(
-  req: SelectPayCoinRequestNg,
+  req: SelectGreedyRequest,
   candidateDenoms: AvailableDenom[],
-  wireFeesPerExchange: Record<string, AmountJson>,
   tally: CoinSelectionTally,
 ): SelResult | undefined {
   const { wireFeeAmortization } = req;
@@ -449,9 +461,9 @@ function selectGreedy(
       i < denom.numAvailable && Amounts.isNonZero(tally.amountPayRemaining);
       i++
     ) {
-      tally = tallyFees(
+      tallyFees(
         tally,
-        wireFeesPerExchange,
+        req.wireFeesPerExchange,
         wireFeeAmortization,
         denom.exchangeBaseUrl,
         Amounts.parseOrThrow(denom.feeDeposit),
@@ -491,6 +503,7 @@ function selectGreedy(
       selectedDenom[avKey] = sd;
     }
   }
+  logger.info(`greedy tally: ${j2s(tally)}`);
   return Amounts.isZero(tally.amountPayRemaining) ? selectedDenom : undefined;
 }
 
@@ -566,9 +579,8 @@ export function checkAccountRestriction(
 }
 
 export interface SelectPayCoinRequestNg {
-  exchanges: AllowedExchangeInfo[];
-  auditors: AllowedAuditorInfo[];
-  wireMethod: string;
+  restrictExchanges: ExchangeRestrictionSpec | undefined;
+  restrictWireMethod: string;
   contractTermsAmount: AmountJson;
   depositFeeLimit: AmountJson;
   wireFeeLimit: AmountJson;
@@ -592,137 +604,183 @@ export type AvailableDenom = DenominationInfo & {
   numAvailable: number;
 };
 
-async function selectPayMerchantCandidates(
+function findMatchingWire(
+  wireMethod: string,
+  depositPaytoUri: string | undefined,
+  exchangeWireDetails: ExchangeWireDetails,
+): { wireFee: AmountJson } | undefined {
+  for (const acc of exchangeWireDetails.wireInfo.accounts) {
+    const pp = parsePaytoUri(acc.payto_uri);
+    checkLogicInvariant(!!pp);
+    if (pp.targetType !== wireMethod) {
+      continue;
+    }
+    const wireFeeStr = exchangeWireDetails.wireInfo.feesForType[
+      wireMethod
+    ]?.find((x) => {
+      return AbsoluteTime.isBetween(
+        AbsoluteTime.now(),
+        AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+        AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+      );
+    })?.wireFee;
+
+    if (!wireFeeStr) {
+      continue;
+    }
+
+    let debitAccountCheckOk = false;
+    if (depositPaytoUri) {
+      // FIXME: We should somehow propagate the hint here!
+      const checkResult = checkAccountRestriction(
+        depositPaytoUri,
+        acc.debit_restrictions,
+      );
+      if (checkResult.ok) {
+        debitAccountCheckOk = true;
+      }
+    } else {
+      debitAccountCheckOk = true;
+    }
+
+    if (!debitAccountCheckOk) {
+      continue;
+    }
+
+    return {
+      wireFee: Amounts.parseOrThrow(wireFeeStr),
+    };
+  }
+  return undefined;
+}
+
+function checkExchangeAccepted(
+  exchangeDetails: ExchangeWireDetails,
+  exchangeRestrictions: ExchangeRestrictionSpec | undefined,
+): boolean {
+  if (!exchangeRestrictions) {
+    return true;
+  }
+  let accepted = false;
+  for (const allowedExchange of exchangeRestrictions.exchanges) {
+    if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+      accepted = true;
+      break;
+    }
+  }
+  for (const allowedAuditor of exchangeRestrictions.auditors) {
+    for (const providedAuditor of exchangeDetails.auditors) {
+      if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
+        accepted = true;
+        break;
+      }
+    }
+  }
+  return accepted;
+}
+
+interface SelectPayCandidatesRequest {
+  instructedAmount: AmountJson;
+  restrictWireMethod: string | undefined;
+  depositPaytoUri?: string;
+  restrictExchanges:
+    | {
+        exchanges: AllowedExchangeInfo[];
+        auditors: AllowedAuditorInfo[];
+      }
+    | undefined;
+  requiredMinimumAge?: number;
+}
+
+async function selectPayCandidates(
   wex: WalletExecutionContext,
-  req: SelectPayCoinRequestNg,
+  tx: WalletDbReadOnlyTransaction<
+    ["exchanges", "coinAvailability", "exchangeDetails", "denominations"]
+  >,
+  req: SelectPayCandidatesRequest,
 ): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
-  return await wex.db.runReadOnlyTx(
-    ["exchanges", "exchangeDetails", "denominations", "coinAvailability"],
-    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> = {};
-      loopExchange: for (const exchange of exchanges) {
-        const exchangeDetails = await getExchangeWireDetailsInTx(
-          tx,
-          exchange.baseUrl,
-        );
-        // 1.- exchange has same currency
-        if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
-          continue;
-        }
-        let wireMethodFee: string | undefined;
-        // 2.- exchange supports wire method
-        loopWireAccount: for (const acc of exchangeDetails.wireInfo.accounts) {
-          const pp = parsePaytoUri(acc.payto_uri);
-          checkLogicInvariant(!!pp);
-          if (pp.targetType !== req.wireMethod) {
-            continue;
-          }
-          const wireFeeStr = exchangeDetails.wireInfo.feesForType[
-            req.wireMethod
-          ]?.find((x) => {
-            return AbsoluteTime.isBetween(
-              AbsoluteTime.now(),
-              AbsoluteTime.fromProtocolTimestamp(x.startStamp),
-              AbsoluteTime.fromProtocolTimestamp(x.endStamp),
-            );
-          })?.wireFee;
-          let debitAccountCheckOk = false;
-          if (req.depositPaytoUri) {
-            // FIXME: We should somehow propagate the hint here!
-            const checkResult = checkAccountRestriction(
-              req.depositPaytoUri,
-              acc.debit_restrictions,
-            );
-            if (checkResult.ok) {
-              debitAccountCheckOk = true;
-            }
-          } else {
-            debitAccountCheckOk = true;
-          }
-
-          if (wireFeeStr) {
-            wireMethodFee = wireFeeStr;
-            break loopWireAccount;
-          }
-        }
-        if (!wireMethodFee) {
-          continue;
-        }
-        wfPerExchange[exchange.baseUrl] = Amounts.parseOrThrow(wireMethodFee);
-
-        // 3.- exchange is trusted in the exchange list or auditor list
-        let accepted = false;
-        for (const allowedExchange of req.exchanges) {
-          if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) 
{
-            accepted = true;
-            break;
-          }
-        }
-        for (const allowedAuditor of req.auditors) {
-          for (const providedAuditor of exchangeDetails.auditors) {
-            if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
-              accepted = true;
-              break;
-            }
-          }
-        }
-        if (!accepted) {
-          continue;
-        }
-        // 4.- filter coins restricted by age
-        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(
-              [exchangeDetails.exchangeBaseUrl, ageLower, 1],
-              [
-                exchangeDetails.exchangeBaseUrl,
-                ageUpper,
-                Number.MAX_SAFE_INTEGER,
-              ],
-            ),
-          );
-        // 5.- save denoms with how many coins are available
-        // FIXME: Check that the individual denomination is audited!
-        // FIXME: Should we exclude denominations that are
-        // not spendable anymore?
-        for (const coinAvail of myExchangeCoins) {
-          const denom = await tx.denominations.get([
-            coinAvail.exchangeBaseUrl,
-            coinAvail.denomPubHash,
-          ]);
-          checkDbInvariant(!!denom);
-          if (denom.isRevoked || !denom.isOffered) {
-            continue;
-          }
-          denoms.push({
-            ...DenominationRecord.toDenomInfo(denom),
-            numAvailable: coinAvail.freshCoinCount ?? 0,
-            maxAge: coinAvail.maxAge,
-          });
-        }
+  // 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> = {};
+  for (const exchange of exchanges) {
+    const exchangeDetails = await getExchangeWireDetailsInTx(
+      tx,
+      exchange.baseUrl,
+    );
+    // 1. exchange has same currency
+    if (exchangeDetails?.currency !== req.instructedAmount.currency) {
+      continue;
+    }
+
+    // 2. Exchange supports wire method (only for pay/deposit)
+    if (req.restrictWireMethod) {
+      const wire = findMatchingWire(
+        req.restrictWireMethod,
+        req.depositPaytoUri,
+        exchangeDetails,
+      );
+      if (!wire) {
+        continue;
       }
-      logger.info(`available denoms ${j2s(denoms)}`);
-      // Sort by available amount (descending),  deposit fee (ascending) and
-      // denomPub (ascending) if deposit fee is the same
-      // (to guarantee deterministic results)
-      denoms.sort(
-        (o1, o2) =>
-          -Amounts.cmp(o1.value, o2.value) ||
-          Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
-          strcmp(o1.denomPubHash, o2.denomPubHash),
+    }
+
+    // 3. exchange is trusted in the exchange list or auditor list
+    let accepted = checkExchangeAccepted(
+      exchangeDetails,
+      req.restrictExchanges,
+    );
+    if (!accepted) {
+      continue;
+    }
+
+    // 4. filter coins restricted by age
+    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(
+          [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+          [exchangeDetails.exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+        ),
       );
-      return [denoms, wfPerExchange];
-    },
+
+    // 5. save denoms with how many coins are available
+    // FIXME: Check that the individual denomination is audited!
+    // FIXME: Should we exclude denominations that are
+    // not spendable anymore?
+    for (const coinAvail of myExchangeCoins) {
+      const denom = await tx.denominations.get([
+        coinAvail.exchangeBaseUrl,
+        coinAvail.denomPubHash,
+      ]);
+      checkDbInvariant(!!denom);
+      if (denom.isRevoked || !denom.isOffered) {
+        continue;
+      }
+      denoms.push({
+        ...DenominationRecord.toDenomInfo(denom),
+        numAvailable: coinAvail.freshCoinCount ?? 0,
+        maxAge: coinAvail.maxAge,
+      });
+    }
+  }
+  logger.info(`available denoms ${j2s(denoms)}`);
+  // Sort by available amount (descending),  deposit fee (ascending) and
+  // denomPub (ascending) if deposit fee is the same
+  // (to guarantee deterministic results)
+  denoms.sort(
+    (o1, o2) =>
+      -Amounts.cmp(o1.value, o2.value) ||
+      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+      strcmp(o1.denomPubHash, o2.denomPubHash),
   );
+  return [denoms, wfPerExchange];
 }
 
 /**
@@ -882,7 +940,7 @@ export type SelectPeerCoinsResult =
   | { type: "success"; result: PeerCoinSelectionDetails }
   | {
       type: "failure";
-      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+      insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
     };
 
 export interface PeerCoinRepair {
@@ -901,134 +959,134 @@ export interface PeerCoinSelectionRequest {
   repair?: PeerCoinRepair;
 }
 
-/**
- * Get coin availability information for a certain exchange.
- */
-async function selectPayPeerCandidatesForExchange(
-  wex: WalletExecutionContext,
-  tx: WalletDbReadOnlyTransaction<["coinAvailability", "denominations"]>,
+async function assemblePeerCoinSelectionDetails(
+  tx: WalletDbReadOnlyTransaction<["coins"]>,
   exchangeBaseUrl: string,
-): Promise<AvailableDenom[]> {
-  const denoms: AvailableDenom[] = [];
-
-  let ageLower = 0;
-  let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
-  const myExchangeCoins =
-    await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
-      GlobalIDB.KeyRange.bound(
-        [exchangeBaseUrl, ageLower, 1],
-        [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+  selectedDenom: SelResult,
+  resCoins: ResCoin[],
+  tally: CoinSelectionTally,
+): Promise<PeerCoinSelectionDetails> {
+  let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
+  for (const dph of Object.keys(selectedDenom)) {
+    const selInfo = selectedDenom[dph];
+    // 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,
+        }),
       ),
     );
-
-  for (const coinAvail of myExchangeCoins) {
-    if (coinAvail.freshCoinCount <= 0) {
-      continue;
+    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})`,
+      );
     }
-    const denom = await tx.denominations.get([
-      coinAvail.exchangeBaseUrl,
-      coinAvail.denomPubHash,
-    ]);
-    checkDbInvariant(!!denom);
-    if (denom.isRevoked || !denom.isOffered) {
-      continue;
+    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,
+      });
     }
-    denoms.push({
-      ...DenominationRecord.toDenomInfo(denom),
-      numAvailable: coinAvail.freshCoinCount ?? 0,
-      maxAge: coinAvail.maxAge,
-    });
   }
-  // Sort by available amount (descending),  deposit fee (ascending) and
-  // denomPub (ascending) if deposit fee is the same
-  // (to guarantee deterministic results)
-  denoms.sort(
-    (o1, o2) =>
-      -Amounts.cmp(o1.value, o2.value) ||
-      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
-      strcmp(o1.denomPubHash, o2.denomPubHash),
-  );
-
-  return denoms;
-}
 
-export interface PeerCoinSelectionTally {
-  amountRemaining: AmountJson;
-  depositFeesAcc: AmountJson;
-  lastDepositFee: AmountJson;
-}
-
-/**
- * exporting for testing
- */
-export function testing_greedySelectPeer(
-  ...args: Parameters<typeof greedySelectPeer>
-): ReturnType<typeof greedySelectPeer> {
-  return greedySelectPeer(...args);
+  return {
+    exchangeBaseUrl,
+    coins: resCoins,
+    depositFees: tally.customerDepositFees,
+    maxExpirationDate: minAutorefreshExecuteThreshold,
+  };
 }
 
-function greedySelectPeer(
-  candidates: AvailableDenom[],
-  tally: PeerCoinSelectionTally,
-): SelResult | undefined {
-  const selectedDenom: SelResult = {};
-  for (const denom of candidates) {
-    const contributions: AmountJson[] = [];
-    const feeDeposit = Amounts.parseOrThrow(denom.feeDeposit);
-    for (
-      let i = 0;
-      i < denom.numAvailable && Amounts.isNonZero(tally.amountRemaining);
-      i++
-    ) {
-      tally.depositFeesAcc = Amounts.add(
-        tally.depositFeesAcc,
-        feeDeposit,
-      ).amount;
-      tally.amountRemaining = Amounts.add(
-        tally.amountRemaining,
-        feeDeposit,
-      ).amount;
-      tally.lastDepositFee = feeDeposit;
-
-      const coinSpend = Amounts.max(
-        Amounts.min(tally.amountRemaining, denom.value),
-        denom.feeDeposit,
+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,
       );
-
-      tally.amountRemaining = Amounts.sub(
-        tally.amountRemaining,
-        coinSpend,
+      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;
-
-      contributions.push(coinSpend);
-    }
-    if (contributions.length > 0) {
-      const avKey = makeAvailabilityKey(
-        denom.exchangeBaseUrl,
-        denom.denomPubHash,
-        denom.maxAge,
-      );
-      let sd = selectedDenom[avKey];
-      if (!sd) {
-        sd = {
-          contributions: [],
-          denomPubHash: denom.denomPubHash,
-          exchangeBaseUrl: denom.exchangeBaseUrl,
-          maxAge: denom.maxAge,
-          expireDeposit: denom.stampExpireDeposit,
-          expireWithdraw: denom.stampExpireWithdraw,
-        };
-      }
-      sd.contributions.push(...contributions);
-      selectedDenom[avKey] = sd;
     }
   }
+  return resCoins;
+}
 
-  if (Amounts.isZero(tally.amountRemaining)) {
-    return selectedDenom;
-  }
+interface ResCoin {
+  coinPub: string;
+  coinPriv: string;
+  contribution: AmountString;
+  denomPubHash: string;
+  denomSig: UnblindedSignature;
+  ageCommitmentProof: AgeCommitmentProof | undefined;
+}
 
-  return undefined;
+export function emptyTallyForPeerPayment(
+  instructedAmount: AmountJson,
+): CoinSelectionTally {
+  const currency = instructedAmount.currency;
+  const zero = Amounts.zeroOfCurrency(currency);
+  return {
+    amountPayRemaining: instructedAmount,
+    customerDepositFees: zero,
+    lastDepositFee: zero,
+    amountDepositFeeLimitRemaining: zero,
+    amountWireFeeLimitRemaining: zero,
+    customerWireFees: zero,
+    wireFeeCoveredForExchange: new Set(),
+  };
 }
 
 export async function selectPeerCoins(
@@ -1041,6 +1099,7 @@ export async function selectPeerCoins(
     // one coin to spend.
     throw new Error("amount of zero not allowed");
   }
+
   return await wex.db.runReadWriteTx(
     [
       "exchanges",
@@ -1049,72 +1108,40 @@ export async function selectPeerCoins(
       "coinAvailability",
       "denominations",
       "refreshGroups",
-      "peerPushDebit",
+      "exchangeDetails",
     ],
-    async (tx) => {
+    async (tx): Promise<SelectPeerCoinsResult> => {
       const exchanges = await tx.exchanges.iter().toArray();
-      const exchangeFeeGap: { [url: string]: AmountJson } = {};
       const currency = Amounts.currencyOf(instructedAmount);
       for (const exch of exchanges) {
         if (exch.detailsPointer?.currency !== currency) {
           continue;
         }
-        const candidates = await selectPayPeerCandidatesForExchange(
+        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: ResCoin[] = await maybeRepairPeerCoinSelection(
           wex,
           tx,
           exch.baseUrl,
+          tally,
+          req.repair,
         );
-        if (logger.shouldLogTrace()) {
-          logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
-        }
-        const tally: PeerCoinSelectionTally = {
-          amountRemaining: Amounts.parseOrThrow(instructedAmount),
-          depositFeesAcc: Amounts.zeroOfCurrency(currency),
-          lastDepositFee: Amounts.zeroOfCurrency(currency),
-        };
-        const resCoins: {
-          coinPub: string;
-          coinPriv: string;
-          contribution: AmountString;
-          denomPubHash: string;
-          denomSig: UnblindedSignature;
-          ageCommitmentProof: AgeCommitmentProof | undefined;
-        }[] = [];
-
-        if (req.repair && req.repair.exchangeBaseUrl === exch.baseUrl) {
-          for (let i = 0; i < req.repair.coinPubs.length; i++) {
-            const contrib = req.repair.contribs[i];
-            const coin = await tx.coins.get(req.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.amountRemaining = Amounts.sub(
-              tally.amountRemaining,
-              Amounts.sub(contrib, depositFee).amount,
-            ).amount;
-            tally.depositFeesAcc = Amounts.add(
-              tally.depositFeesAcc,
-              depositFee,
-            ).amount;
-          }
-        }
 
         if (logger.shouldLogTrace()) {
           logger.trace(`candidates: ${j2s(candidates)}`);
@@ -1122,113 +1149,42 @@ export async function selectPeerCoins(
           logger.trace(`tally: ${j2s(tally)}`);
         }
 
-        const selectedDenom = greedySelectPeer(candidates, tally);
+        const selectedDenom = selectGreedy(
+          {
+            wireFeeAmortization: 1,
+            wireFeesPerExchange: {},
+          },
+          candidates,
+          tally,
+        );
 
         if (selectedDenom) {
-          let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
-          for (const dph of Object.keys(selectedDenom)) {
-            const selInfo = selectedDenom[dph];
-            // 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,
-                }),
-              ),
-            );
-            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,
-              });
-            }
-          }
-
-          const res: PeerCoinSelectionDetails = {
-            exchangeBaseUrl: exch.baseUrl,
-            coins: resCoins,
-            depositFees: tally.depositFeesAcc,
-            maxExpirationDate: minAutorefreshExecuteThreshold,
+          return {
+            type: "success",
+            result: await assemblePeerCoinSelectionDetails(
+              tx,
+              exch.baseUrl,
+              selectedDenom,
+              resCoins,
+              tally,
+            ),
           };
-          return { type: "success", result: res };
-        }
-
-        exchangeFeeGap[exch.baseUrl] = Amounts.add(
-          tally.lastDepositFee,
-          tally.amountRemaining,
-        ).amount;
-
-        continue;
-      }
-
-      // We were unable to select coins.
-      // Now we need to produce error details.
-
-      const infoGeneral = await getPeerPaymentBalanceDetailsInTx(wex, tx, {
-        currency,
-      });
-
-      const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
-
-      let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
-
-      for (const exch of exchanges) {
-        if (exch.detailsPointer?.currency !== currency) {
-          continue;
-        }
-        const infoExchange = await getPeerPaymentBalanceDetailsInTx(wex, tx, {
-          currency,
-          restrictExchangeTo: exch.baseUrl,
-        });
-        let gap =
-          exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
-        if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
-          // Show fee gap only if we should've been able to pay with the 
material amount
-          gap = Amounts.zeroOfCurrency(currency);
         }
-        perExchange[exch.baseUrl] = {
-          balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
-          balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
-          feeGapEstimate: Amounts.stringify(gap),
-        };
-
-        maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
       }
-
-      const errDetails: PayPeerInsufficientBalanceDetails = {
-        amountRequested: Amounts.stringify(instructedAmount),
-        balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
-        balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
-        feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
-        perExchange,
+      const insufficientBalanceDetails = await 
reportInsufficientBalanceDetails(
+        wex,
+        tx,
+        {
+          restrictExchanges: undefined,
+          instructedAmount: req.instructedAmount,
+          requiredMinimumAge: undefined,
+          wireMethod: undefined,
+        },
+      );
+      return {
+        type: "failure",
+        insufficientBalanceDetails,
       };
-
-      return { type: "failure", insufficientBalanceDetails: errDetails };
     },
   );
 }
diff --git a/packages/taler-wallet-core/src/deposits.ts 
b/packages/taler-wallet-core/src/deposits.ts
index 960b123c6..2e28ba9b7 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -72,7 +72,7 @@ import {
   stringToBytes,
 } from "@gnu-taler/taler-util";
 import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { selectPayCoinsNew } from "./coinSelection.js";
+import { selectPayCoins } from "./coinSelection.js";
 import {
   PendingTaskType,
   TaskIdStr,
@@ -1219,10 +1219,12 @@ export async function prepareDepositGroup(
     "",
   );
 
-  const payCoinSel = await selectPayCoinsNew(wex, {
-    auditors: [],
-    exchanges: contractData.allowedExchanges,
-    wireMethod: contractData.wireMethod,
+  const payCoinSel = await selectPayCoins(wex, {
+    restrictExchanges: {
+      auditors: [],
+      exchanges: contractData.allowedExchanges,
+    },
+    restrictWireMethod: contractData.wireMethod,
     contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
     depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -1338,10 +1340,12 @@ export async function createDepositGroup(
     "",
   );
 
-  const payCoinSel = await selectPayCoinsNew(wex, {
-    auditors: [],
-    exchanges: contractData.allowedExchanges,
-    wireMethod: contractData.wireMethod,
+  const payCoinSel = await selectPayCoins(wex, {
+    restrictExchanges: {
+      auditors: [],
+      exchanges: contractData.allowedExchanges,
+    },
+    restrictWireMethod: contractData.wireMethod,
     contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
     depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts 
b/packages/taler-wallet-core/src/pay-merchant.ts
index a3623e6d2..ed58dc404 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -96,7 +96,7 @@ import {
   readUnexpectedResponseDetails,
   throwUnexpectedRequestError,
 } from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPayCoinsNew } from "./coinSelection.js";
+import { PreviousPayCoins, selectPayCoins } from "./coinSelection.js";
 import {
   constructTaskIdentifier,
   PendingTaskType,
@@ -1161,10 +1161,12 @@ async function handleInsufficientFunds(
     }
   });
 
-  const res = await selectPayCoinsNew(wex, {
-    auditors: [],
-    exchanges: contractData.allowedExchanges,
-    wireMethod: contractData.wireMethod,
+  const res = await selectPayCoins(wex, {
+    restrictExchanges: {
+      auditors: [],
+      exchanges: contractData.allowedExchanges,
+    },
+    restrictWireMethod: contractData.wireMethod,
     contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
     depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -1285,16 +1287,18 @@ async function checkPaymentByProposalId(
     purchase.purchaseStatus === PurchaseStatus.DialogShared
   ) {
     // If not already paid, check if we could pay for it.
-    const res = await selectPayCoinsNew(wex, {
-      auditors: [],
-      exchanges: contractData.allowedExchanges,
+    const res = await selectPayCoins(wex, {
+      restrictExchanges: {
+        auditors: [],
+        exchanges: contractData.allowedExchanges,
+      },
       contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
       depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
       wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
       wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
       prevPayCoins: [],
       requiredMinimumAge: contractData.minimumAge,
-      wireMethod: contractData.wireMethod,
+      restrictWireMethod: contractData.wireMethod,
     });
 
     if (res.type !== "success") {
@@ -1820,10 +1824,12 @@ export async function confirmPay(
 
   const contractData = d.contractData;
 
-  const selectCoinsResult = await selectPayCoinsNew(wex, {
-    auditors: [],
-    exchanges: contractData.allowedExchanges,
-    wireMethod: contractData.wireMethod,
+  const selectCoinsResult = await selectPayCoins(wex, {
+    restrictExchanges: {
+      auditors: [],
+      exchanges: contractData.allowedExchanges,
+    },
+    restrictWireMethod: contractData.wireMethod,
     contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
     depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index eee5fb684..a3f00f164 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -60,6 +60,7 @@ export const NoEnoughBalanceAvailable = 
tests.createExample(BaseView, {
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
       feeGapEstimate: "USD:1" as AmountString,
+      perExchange: {},
     },
     talerUri: "taler://pay/..",
 
@@ -100,6 +101,7 @@ export const NoEnoughBalanceMaterial = 
tests.createExample(BaseView, {
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:0" as AmountString,
       feeGapEstimate: "USD:1" as AmountString,
+      perExchange: {},
     },
     talerUri: "taler://pay/..",
 
@@ -140,6 +142,7 @@ export const NoEnoughBalanceAgeAcceptable = 
tests.createExample(BaseView, {
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
       feeGapEstimate: "USD:1" as AmountString,
+      perExchange: {},
     },
     talerUri: "taler://pay/..",
 
@@ -181,6 +184,7 @@ export const NoEnoughBalanceMerchantAcceptable = 
tests.createExample(BaseView, {
       balanceMerchantAcceptable: "USD:9" as AmountString,
       balanceMerchantDepositable: "USD:9" as AmountString,
       feeGapEstimate: "USD:1" as AmountString,
+      perExchange: {},
     },
     talerUri: "taler://pay/..",
 
@@ -223,6 +227,7 @@ export const NoEnoughBalanceMerchantDepositable = 
tests.createExample(
         balanceMerchantAcceptable: "USD:10" as AmountString,
         balanceMerchantDepositable: "USD:9" as AmountString,
         feeGapEstimate: "USD:1" as AmountString,
+        perExchange: {},
       },
       talerUri: "taler://pay/..",
 
@@ -264,6 +269,7 @@ export const NoEnoughBalanceFeeGap = 
tests.createExample(BaseView, {
       balanceMerchantAcceptable: "USD:10" as AmountString,
       balanceMerchantDepositable: "USD:10" as AmountString,
       feeGapEstimate: "USD:1" as AmountString,
+      perExchange: {},
     },
     talerUri: "taler://pay/..",
 

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