gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: wallet-core: split coin selec


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: split coin selection and instructed amount conversion
Date: Tue, 29 Aug 2023 18:33:54 +0200

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 a386de8a9 wallet-core: split coin selection and instructed amount 
conversion
a386de8a9 is described below

commit a386de8a9c1aa3fff76b4cb37fb3287213981387
Author: Florian Dold <florian@dold.me>
AuthorDate: Tue Aug 29 18:33:51 2023 +0200

    wallet-core: split coin selection and instructed amount conversion
---
 .../src/operations/pay-peer-common.ts              |  265 +----
 .../src/operations/pay-peer-pull-debit.ts          |    3 +-
 .../src/operations/pay-peer-push-debit.ts          |    3 +-
 .../src/util/coinSelection.test.ts                 |  742 --------------
 .../taler-wallet-core/src/util/coinSelection.ts    | 1012 +++++---------------
 ....test.ts => instructedAmountConversion.test.ts} |    8 +-
 ...nSelection.ts => instructedAmountConversion.ts} |  845 +---------------
 packages/taler-wallet-core/src/wallet.ts           |   14 +-
 8 files changed, 287 insertions(+), 2605 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/pay-peer-common.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
index 4fdfecb4d..49f255eb9 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-common.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-common.ts
@@ -43,8 +43,6 @@ import {
 import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
 import {
   DenominationRecord,
-  KycPendingInfo,
-  KycUserType,
   PeerPushPaymentCoinSelection,
   ReserveRecord,
 } from "../db.js";
@@ -52,68 +50,13 @@ import { InternalWalletState } from 
"../internal-wallet-state.js";
 import { checkDbInvariant } from "../util/invariants.js";
 import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
 import { getTotalRefreshCost } from "./refresh.js";
+import type { PeerCoinInfo, PeerCoinSelectionRequest, SelectPeerCoinsResult, 
SelectedPeerCoin } from "../util/coinSelection.js";
 
 const logger = new Logger("operations/peer-to-peer.ts");
 
-interface SelectedPeerCoin {
-  coinPub: string;
-  coinPriv: string;
-  contribution: AmountString;
-  denomPubHash: string;
-  denomSig: UnblindedSignature;
-  ageCommitmentProof: AgeCommitmentProof | undefined;
-}
-
-interface PeerCoinSelectionDetails {
-  exchangeBaseUrl: string;
-
-  /**
-   * Info of Coins that were selected.
-   */
-  coins: SelectedPeerCoin[];
-
-  /**
-   * How much of the deposit fees is the customer paying?
-   */
-  depositFees: AmountJson;
-}
-
-/**
- * Information about a selected coin for peer to peer payments.
- */
-interface CoinInfo {
-  /**
-   * Public key of the coin.
-   */
-  coinPub: string;
-
-  coinPriv: string;
-
-  /**
-   * Deposit fee for the coin.
-   */
-  feeDeposit: AmountJson;
-
-  value: AmountJson;
-
-  denomPubHash: string;
-
-  denomSig: UnblindedSignature;
-
-  maxAge: number;
-
-  ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export type SelectPeerCoinsResult =
-  | { type: "success"; result: PeerCoinSelectionDetails }
-  | {
-      type: "failure";
-      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
-    };
-
 /**
  * Get information about the coin selected for signatures
+ *
  * @param ws
  * @param csel
  * @returns
@@ -153,211 +96,7 @@ export async function queryCoinInfosForSelection(
   return infos;
 }
 
-export interface PeerCoinRepair {
-  exchangeBaseUrl: string;
-  coinPubs: CoinPublicKeyString[];
-  contribs: AmountJson[];
-}
-
-export interface PeerCoinSelectionRequest {
-  instructedAmount: AmountJson;
 
-  /**
-   * Instruct the coin selection to repair this coin
-   * selection instead of selecting completely new coins.
-   */
-  repair?: PeerCoinRepair;
-}
-
-export async function selectPeerCoins(
-  ws: InternalWalletState,
-  req: PeerCoinSelectionRequest,
-): Promise<SelectPeerCoinsResult> {
-  const instructedAmount = req.instructedAmount;
-  if (Amounts.isZero(instructedAmount)) {
-    // Other parts of the code assume that we have at least
-    // one coin to spend.
-    throw new Error("amount of zero not allowed");
-  }
-  return await ws.db
-    .mktx((x) => [
-      x.exchanges,
-      x.contractTerms,
-      x.coins,
-      x.coinAvailability,
-      x.denominations,
-      x.refreshGroups,
-      x.peerPushPaymentInitiations,
-    ])
-    .runReadWrite(async (tx) => {
-      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;
-        }
-        // FIXME: Can't we do this faster by using coinAvailability?
-        const coins = (
-          await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
-        ).filter((x) => x.status === CoinStatus.Fresh);
-        const coinInfos: CoinInfo[] = [];
-        for (const coin of coins) {
-          const denom = await ws.getDenomInfo(
-            ws,
-            tx,
-            coin.exchangeBaseUrl,
-            coin.denomPubHash,
-          );
-          if (!denom) {
-            throw Error("denom not found");
-          }
-          coinInfos.push({
-            coinPub: coin.coinPub,
-            feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
-            value: Amounts.parseOrThrow(denom.value),
-            denomPubHash: denom.denomPubHash,
-            coinPriv: coin.coinPriv,
-            denomSig: coin.denomSig,
-            maxAge: coin.maxAge,
-            ageCommitmentProof: coin.ageCommitmentProof,
-          });
-        }
-        if (coinInfos.length === 0) {
-          continue;
-        }
-        coinInfos.sort(
-          (o1, o2) =>
-            -Amounts.cmp(o1.value, o2.value) ||
-            strcmp(o1.denomPubHash, o2.denomPubHash),
-        );
-        let amountAcc = Amounts.zeroOfCurrency(currency);
-        let depositFeesAcc = Amounts.zeroOfCurrency(currency);
-        const resCoins: {
-          coinPub: string;
-          coinPriv: string;
-          contribution: AmountString;
-          denomPubHash: string;
-          denomSig: UnblindedSignature;
-          ageCommitmentProof: AgeCommitmentProof | undefined;
-        }[] = [];
-        let lastDepositFee = Amounts.zeroOfCurrency(currency);
-
-        if (req.repair) {
-          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 ws.getDenomInfo(
-              ws,
-              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);
-            lastDepositFee = depositFee;
-            amountAcc = Amounts.add(
-              amountAcc,
-              Amounts.sub(contrib, depositFee).amount,
-            ).amount;
-            depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
-          }
-        }
-
-        for (const coin of coinInfos) {
-          if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
-            break;
-          }
-          const gap = Amounts.add(
-            coin.feeDeposit,
-            Amounts.sub(instructedAmount, amountAcc).amount,
-          ).amount;
-          const contrib = Amounts.min(gap, coin.value);
-          amountAcc = Amounts.add(
-            amountAcc,
-            Amounts.sub(contrib, coin.feeDeposit).amount,
-          ).amount;
-          depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
-          resCoins.push({
-            coinPriv: coin.coinPriv,
-            coinPub: coin.coinPub,
-            contribution: Amounts.stringify(contrib),
-            denomPubHash: coin.denomPubHash,
-            denomSig: coin.denomSig,
-            ageCommitmentProof: coin.ageCommitmentProof,
-          });
-          lastDepositFee = coin.feeDeposit;
-        }
-        if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
-          const res: PeerCoinSelectionDetails = {
-            exchangeBaseUrl: exch.baseUrl,
-            coins: resCoins,
-            depositFees: depositFeesAcc,
-          };
-          return { type: "success", result: res };
-        }
-        const diff = Amounts.sub(instructedAmount, amountAcc).amount;
-        exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, 
diff).amount;
-
-        continue;
-      }
-
-      // We were unable to select coins.
-      // Now we need to produce error details.
-
-      const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, 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(ws, 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,
-      };
-
-      return { type: "failure", insufficientBalanceDetails: errDetails };
-    });
-}
 
 export async function getTotalPeerPaymentCost(
   ws: InternalWalletState,
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
index 8ba84585c..0de91bf97 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts
@@ -68,11 +68,9 @@ import {
   spendCoins,
 } from "./common.js";
 import {
-  PeerCoinRepair,
   codecForExchangePurseStatus,
   getTotalPeerPaymentCost,
   queryCoinInfosForSelection,
-  selectPeerCoins,
 } from "./pay-peer-common.js";
 import {
   constructTransactionIdentifier,
@@ -80,6 +78,7 @@ import {
   parseTransactionIdentifier,
   stopLongpolling,
 } from "./transactions.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
 
 const logger = new Logger("pay-peer-pull-debit.ts");
 
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts 
b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
index c853bc0ef..2349e5c4a 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts
@@ -68,17 +68,16 @@ import {
   spendCoins,
 } from "./common.js";
 import {
-  PeerCoinRepair,
   codecForExchangePurseStatus,
   getTotalPeerPaymentCost,
   queryCoinInfosForSelection,
-  selectPeerCoins,
 } from "./pay-peer-common.js";
 import {
   constructTransactionIdentifier,
   notifyTransition,
   stopLongpolling,
 } from "./transactions.js";
+import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
 
 const logger = new Logger("pay-peer-push-debit.ts");
 
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index fddd217ea..b907eb160 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -22,746 +22,4 @@ import {
   TransactionAmountMode,
 } from "@gnu-taler/taler-util";
 import test, { ExecutionContext } from "ava";
-import {
-  CoinInfo,
-  convertDepositAmountForAvailableCoins,
-  convertWithdrawalAmountFromAvailableCoins,
-  getMaxDepositAmountForAvailableCoins,
-} from "./coinSelection.js";
-
-function makeCurrencyHelper(currency: string) {
-  return (sx: TemplateStringsArray, ...vx: any[]) => {
-    const s = String.raw({ raw: sx }, ...vx);
-    return Amounts.parseOrThrow(`${currency}:${s}`);
-  };
-}
-
-const kudos = makeCurrencyHelper("kudos");
-
-function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo 
{
-  return {
-    id: Amounts.stringify(value),
-    denomDeposit: kudos`0.01`,
-    denomRefresh: kudos`0.01`,
-    denomWithdraw: kudos`0.01`,
-    exchangeBaseUrl: "1",
-    duration: Duration.getForever(),
-    exchangePurse: undefined,
-    exchangeWire: undefined,
-    maxAge: AgeRestriction.AGE_UNRESTRICTED,
-    totalAvailable,
-    value,
-  };
-}
-type Coin = [AmountJson, number];
-
-/**
- * Making a deposit with effective amount
- *
- */
-
-test("deposit effective 2", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`2`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "1.99");
-});
-
-test("deposit effective 10", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`10`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "10");
-  t.is(Amounts.stringifyValue(result.raw), "9.98");
-});
-
-test("deposit effective 24", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`24`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24");
-  t.is(Amounts.stringifyValue(result.raw), "23.94");
-});
-
-test("deposit effective 40", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`40`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "35");
-  t.is(Amounts.stringifyValue(result.raw), "34.9");
-});
-
-test("deposit with wire fee effective 2", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        one: {
-          wireFee: kudos`0.1`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    kudos`2`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "1.89");
-});
-
-/**
- * Making a deposit with raw amount, using the result from effective
- *
- */
-
-test("deposit raw 1.99 (effective 2)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`1.99`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "1.99");
-});
-
-test("deposit raw 9.98 (effective 10)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`9.98`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "10");
-  t.is(Amounts.stringifyValue(result.raw), "9.98");
-});
-
-test("deposit raw 23.94 (effective 24)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`23.94`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24");
-  t.is(Amounts.stringifyValue(result.raw), "23.94");
-});
-
-test("deposit raw 34.9 (effective 40)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`34.9`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "35");
-  t.is(Amounts.stringifyValue(result.raw), "34.9");
-});
-
-test("deposit with wire fee raw 2", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        one: {
-          wireFee: kudos`0.1`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    kudos`2`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "1.89");
-});
-
-/**
- * Calculating the max amount possible to deposit
- *
- */
-
-test("deposit max 35", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = getMaxDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        "2": {
-          wireFee: kudos`0.00`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    "KUDOS",
-  );
-  t.is(Amounts.stringifyValue(result.raw), "34.9");
-  t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max 35 with wirefee", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = getMaxDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        "2": {
-          wireFee: kudos`1`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    "KUDOS",
-  );
-  t.is(Amounts.stringifyValue(result.raw), "33.9");
-  t.is(Amounts.stringifyValue(result.effective), "35");
-});
-
-test("deposit max repeated denom", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 1],
-    [kudos`2`, 1],
-    [kudos`5`, 1],
-  ];
-  const result = getMaxDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        "2": {
-          wireFee: kudos`0.00`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    "KUDOS",
-  );
-  t.is(Amounts.stringifyValue(result.raw), "8.97");
-  t.is(Amounts.stringifyValue(result.effective), "9");
-});
-
-/**
- * Making a withdrawal with effective amount
- *
- */
-
-test("withdraw effective 2", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`2`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "2.01");
-});
-
-test("withdraw effective 10", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`10`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "10");
-  t.is(Amounts.stringifyValue(result.raw), "10.02");
-});
-
-test("withdraw effective 24", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`24`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24");
-  t.is(Amounts.stringifyValue(result.raw), "24.06");
-});
-
-test("withdraw effective 40", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`40`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "40");
-  t.is(Amounts.stringifyValue(result.raw), "40.08");
-});
-
-/**
- * Making a deposit with raw amount, using the result from effective
- *
- */
-
-test("withdraw raw 2.01 (effective 2)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`2.01`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "2");
-  t.is(Amounts.stringifyValue(result.raw), "2.01");
-});
-
-test("withdraw raw 10.02 (effective 10)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`10.02`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "10");
-  t.is(Amounts.stringifyValue(result.raw), "10.02");
-});
-
-test("withdraw raw 24.06 (effective 24)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`24.06`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24");
-  t.is(Amounts.stringifyValue(result.raw), "24.06");
-});
-
-test("withdraw raw 40.08 (effective 40)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`40.08`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "40");
-  t.is(Amounts.stringifyValue(result.raw), "40.08");
-});
-
-test("withdraw raw 25", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 0],
-    [kudos`5`, 0],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`25`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24.8");
-  t.is(Amounts.stringifyValue(result.raw), "24.94");
-});
-
-test("withdraw effective 24.8 (raw 25)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 0],
-    [kudos`5`, 0],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`24.8`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24.8");
-  t.is(Amounts.stringifyValue(result.raw), "24.94");
-});
-
-/**
- * Making a deposit with refresh
- *
- */
-
-test("deposit with refresh: effective 3", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`3`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "3.1");
-  t.is(Amounts.stringifyValue(result.raw), "2.98");
-  expectDefined(t, result.refresh);
-  //FEES
-  //deposit  2 x 0.01
-  //refresh  1 x 0.01
-  //withdraw 9 x 0.01
-  //-----------------
-  //op           0.12
-
-  //coins sent  2 x 2.0
-  //coins recv  9 x 0.1
-  //-------------------
-  //effective       3.10
-  //raw             2.98
-  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
-  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
-});
-
-test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`2.98`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "3.2");
-  t.is(Amounts.stringifyValue(result.raw), "3.09");
-  expectDefined(t, result.refresh);
-  //FEES
-  //deposit  1 x 0.01
-  //refresh  1 x 0.01
-  //withdraw 8 x 0.01
-  //-----------------
-  //op           0.10
-
-  //coins sent  1 x 2.0
-  //coins recv  8 x 0.1
-  //-------------------
-  //effective       3.20
-  //raw             3.09
-  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
-  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
-});
-
-test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 5],
-    [kudos`5`, 5],
-  ];
-  const result = convertDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`3.2`,
-    TransactionAmountMode.Effective,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "3.3");
-  t.is(Amounts.stringifyValue(result.raw), "3.2");
-  expectDefined(t, result.refresh);
-  //FEES
-  //deposit  2 x 0.01
-  //refresh  1 x 0.01
-  //withdraw 7 x 0.01
-  //-----------------
-  //op           0.10
-
-  //coins sent  2 x 2.0
-  //coins recv  7 x 0.1
-  //-------------------
-  //effective       3.30
-  //raw             3.20
-  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
-  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
-});
-
-function expectDefined<T>(
-  t: ExecutionContext,
-  v: T | undefined,
-): asserts v is T {
-  t.assert(v !== undefined);
-}
-
-function asCoinList(v: { info: CoinInfo; size: number }[]): any {
-  return v.map((c) => {
-    return [c.info.value, c.size];
-  });
-}
-
-/**
- * regression tests
- */
-
-test("demo: withdraw raw 25", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 0],
-    [kudos`5`, 0],
-    [kudos`10`, 0],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`25`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24.8");
-  t.is(Amounts.stringifyValue(result.raw), "24.92");
-  // coins received
-  // 8 x  0.1
-  // 2 x  0.2
-  // 2 x 10.0
-  // total effective 24.8
-  // fee 12 x 0.01 = 0.12
-  // total raw 24.92
-  // left in reserve 25 - 24.92 == 0.08
-
-  //current wallet impl: hides the left in reserve fee
-  //shows fee = 0.2
-});
-
-test("demo: deposit max after withdraw raw 25", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 8],
-    [kudos`1`, 0],
-    [kudos`2`, 2],
-    [kudos`5`, 0],
-    [kudos`10`, 2],
-  ];
-  const result = getMaxDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        one: {
-          wireFee: kudos`0.01`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    "KUDOS",
-  );
-  t.is(Amounts.stringifyValue(result.effective), "24.8");
-  t.is(Amounts.stringifyValue(result.raw), "24.67");
-
-  // 8 x  0.1
-  // 2 x  0.2
-  // 2 x 10.0
-  // total effective 24.8
-  // deposit fee 12 x 0.01 = 0.12
-  // wire fee 0.01
-  // total raw: 24.8 - 0.13 = 24.67
-
-  // current wallet impl fee 0.14
-});
-
-test("demo: withdraw raw 13", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 0],
-    [kudos`1`, 0],
-    [kudos`2`, 0],
-    [kudos`5`, 0],
-    [kudos`10`, 0],
-  ];
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {},
-    },
-    kudos`13`,
-    TransactionAmountMode.Raw,
-  );
-  t.is(Amounts.stringifyValue(result.effective), "12.8");
-  t.is(Amounts.stringifyValue(result.raw), "12.9");
-  // coins received
-  // 8 x  0.1
-  // 1 x  0.2
-  // 1 x 10.0
-  // total effective 12.8
-  // fee 10 x 0.01 = 0.10
-  // total raw 12.9
-  // left in reserve 13 - 12.9 == 0.1
-
-  //current wallet impl: hides the left in reserve fee
-  //shows fee = 0.2
-});
-
-test("demo: deposit max after withdraw raw 13", (t) => {
-  const coinList: Coin[] = [
-    [kudos`0.1`, 8],
-    [kudos`1`, 0],
-    [kudos`2`, 1],
-    [kudos`5`, 0],
-    [kudos`10`, 1],
-  ];
-  const result = getMaxDepositAmountForAvailableCoins(
-    {
-      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        one: {
-          wireFee: kudos`0.01`,
-          purseFee: kudos`0.00`,
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-        },
-      },
-    },
-    "KUDOS",
-  );
-  t.is(Amounts.stringifyValue(result.effective), "12.8");
-  t.is(Amounts.stringifyValue(result.raw), "12.69");
-
-  // 8 x  0.1
-  // 1 x  0.2
-  // 1 x 10.0
-  // total effective 12.8
-  // deposit fee 10 x 0.01 = 0.10
-  // wire fee 0.01
-  // total raw: 12.8 - 0.11 = 12.69
 
-  // current wallet impl fee 0.14
-});
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index d3c6ffc67..bb901fd75 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -31,6 +31,8 @@ import {
   AmountJson,
   AmountResponse,
   Amounts,
+  AmountString,
+  CoinPublicKeyString,
   CoinStatus,
   ConvertAmountRequest,
   DenominationInfo,
@@ -40,28 +42,28 @@ import {
   ForcedCoinSel,
   ForcedDenomSel,
   GetAmountRequest,
-  GetPlanForOperationRequest,
   j2s,
   Logger,
   parsePaytoUri,
   PayCoinSelection,
   PayMerchantInsufficientBalanceDetails,
+  PayPeerInsufficientBalanceDetails,
   strcmp,
   TransactionAmountMode,
   TransactionType,
+  UnblindedSignature,
 } from "@gnu-taler/taler-util";
 import {
   AllowedAuditorInfo,
   AllowedExchangeInfo,
   DenominationRecord,
 } from "../db.js";
-import {
-  CoinAvailabilityRecord,
-  getExchangeDetails,
-  isWithdrawableDenom,
-} from "../index.js";
+import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
-import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
+import {
+  getMerchantPaymentBalanceDetails,
+  getPeerPaymentBalanceDetailsInTx,
+} from "../operations/balance.js";
 import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
 
 const logger = new Logger("coinSelection.ts");
@@ -255,7 +257,7 @@ export async function selectPayCoinsNew(
     wireFeeAmortization,
   } = req;
 
-  const [candidateDenoms, wireFeesPerExchange] = await selectCandidates(
+  const [candidateDenoms, wireFeesPerExchange] = await 
selectPayMerchantCandidates(
     ws,
     req,
   );
@@ -549,7 +551,7 @@ export type AvailableDenom = DenominationInfo & {
   numAvailable: number;
 };
 
-async function selectCandidates(
+async function selectPayMerchantCandidates(
   ws: InternalWalletState,
   req: SelectPayCoinRequestNg,
 ): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
@@ -797,76 +799,6 @@ export function selectForcedWithdrawalDenominations(
   };
 }
 
-function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
-  switch (req.type) {
-    case TransactionType.Withdrawal: {
-      return {
-        exchanges:
-          req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
-      };
-    }
-    case TransactionType.Deposit: {
-      const payto = parsePaytoUri(req.account);
-      if (!payto) {
-        throw Error(`wrong payto ${req.account}`);
-      }
-      return {
-        wireMethod: payto.targetType,
-      };
-    }
-  }
-}
-
-/**
- * If the operation going to be plan subtracts
- * or adds amount in the wallet db
- */
-export enum OperationType {
-  Credit = "credit",
-  Debit = "debit",
-}
-
-function getOperationType(txType: TransactionType): OperationType {
-  const operationType =
-    txType === TransactionType.Withdrawal
-      ? OperationType.Credit
-      : txType === TransactionType.Deposit
-      ? OperationType.Debit
-      : undefined;
-  if (!operationType) {
-    throw Error(`operation type ${txType} not yet supported`);
-  }
-  return operationType;
-}
-
-interface RefreshChoice {
-  /**
-   * Amount that need to be covered
-   */
-  gap: AmountJson;
-  totalFee: AmountJson;
-  selected: CoinInfo;
-  totalChangeValue: AmountJson;
-  refreshEffective: AmountJson;
-  coins: { info: CoinInfo; size: number }[];
-
-  // totalValue: AmountJson;
-  // totalDepositFee: AmountJson;
-  // totalRefreshFee: AmountJson;
-  // totalChangeContribution: AmountJson;
-  // totalChangeWithdrawalFee: AmountJson;
-}
-
-interface AvailableCoins {
-  list: CoinInfo[];
-  exchanges: Record<string, ExchangeInfo>;
-}
-interface SelectedCoins {
-  totalValue: AmountJson;
-  coins: { info: CoinInfo; size: number }[];
-  refresh?: RefreshChoice;
-}
-
 export interface CoinInfo {
   id: string;
   value: AmountJson;
@@ -880,739 +812,267 @@ export interface CoinInfo {
   exchangeBaseUrl: string;
   maxAge: number;
 }
-interface ExchangeInfo {
-  wireFee: AmountJson | undefined;
-  purseFee: AmountJson | undefined;
-  creditDeadline: AbsoluteTime;
-  debitDeadline: AbsoluteTime;
-}
-
-interface CoinsFilter {
-  shouldCalculatePurseFee?: boolean;
-  exchanges?: string[];
-  wireMethod?: string;
-  ageRestricted?: number;
-}
-/**
- * Get all the denoms that can be used for a operation that is limited
- * by the following restrictions.
- * This function is costly (by the database access) but with high chances
- * of being cached
- */
-async function getAvailableDenoms(
-  ws: InternalWalletState,
-  op: TransactionType,
-  currency: string,
-  filters: CoinsFilter = {},
-): Promise<AvailableCoins> {
-  const operationType = getOperationType(TransactionType.Deposit);
-
-  return await ws.db
-    .mktx((x) => [
-      x.exchanges,
-      x.exchangeDetails,
-      x.denominations,
-      x.coinAvailability,
-    ])
-    .runReadOnly(async (tx) => {
-      const list: CoinInfo[] = [];
-      const exchanges: Record<string, ExchangeInfo> = {};
-
-      const databaseExchanges = await tx.exchanges.iter().toArray();
-      const filteredExchanges =
-        filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
-
-      for (const exchangeBaseUrl of filteredExchanges) {
-        const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
-        // 1.- exchange has same currency
-        if (exchangeDetails?.currency !== currency) {
-          continue;
-        }
-
-        let deadline = AbsoluteTime.never();
-        // 2.- exchange supports wire method
-        let wireFee: AmountJson | undefined;
-        if (filters.wireMethod) {
-          const wireMethodWithDates =
-            exchangeDetails.wireInfo.feesForType[filters.wireMethod];
-
-          if (!wireMethodWithDates) {
-            throw Error(
-              `exchange ${exchangeBaseUrl} doesn't have wire method 
${filters.wireMethod}`,
-            );
-          }
-          const wireMethodFee = wireMethodWithDates.find((x) => {
-            return AbsoluteTime.isBetween(
-              AbsoluteTime.now(),
-              AbsoluteTime.fromProtocolTimestamp(x.startStamp),
-              AbsoluteTime.fromProtocolTimestamp(x.endStamp),
-            );
-          });
-
-          if (!wireMethodFee) {
-            throw Error(
-              `exchange ${exchangeBaseUrl} doesn't have wire fee defined for 
this period`,
-            );
-          }
-          wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
-          deadline = AbsoluteTime.min(
-            deadline,
-            AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
-          );
-        }
-        // exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
-
-        // 3.- exchange supports wire method
-        let purseFee: AmountJson | undefined;
-        if (filters.shouldCalculatePurseFee) {
-          const purseFeeFound = exchangeDetails.globalFees.find((x) => {
-            return AbsoluteTime.isBetween(
-              AbsoluteTime.now(),
-              AbsoluteTime.fromProtocolTimestamp(x.startDate),
-              AbsoluteTime.fromProtocolTimestamp(x.endDate),
-            );
-          });
-          if (!purseFeeFound) {
-            throw Error(
-              `exchange ${exchangeBaseUrl} doesn't have purse fee defined for 
this period`,
-            );
-          }
-          purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
-          deadline = AbsoluteTime.min(
-            deadline,
-            AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
-          );
-        }
-
-        let creditDeadline = AbsoluteTime.never();
-        let debitDeadline = AbsoluteTime.never();
-        //4.- filter coins restricted by age
-        if (operationType === OperationType.Credit) {
-          const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
-            exchangeBaseUrl,
-          );
-          for (const denom of ds) {
-            const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
-              denom.stampExpireWithdraw,
-            );
-            const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
-              denom.stampExpireDeposit,
-            );
-            creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
-            debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
-            list.push(
-              buildCoinInfoFromDenom(
-                denom,
-                purseFee,
-                wireFee,
-                AgeRestriction.AGE_UNRESTRICTED,
-                Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
-              ),
-            );
-          }
-        } else {
-          const ageLower = filters.ageRestricted ?? 0;
-          const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
-
-          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;
-            }
-            const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
-              denom.stampExpireWithdraw,
-            );
-            const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
-              denom.stampExpireDeposit,
-            );
-            creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
-            debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
-            list.push(
-              buildCoinInfoFromDenom(
-                denom,
-                purseFee,
-                wireFee,
-                coinAvail.maxAge,
-                coinAvail.freshCoinCount,
-              ),
-            );
-          }
-        }
-
-        exchanges[exchangeBaseUrl] = {
-          purseFee,
-          wireFee,
-          debitDeadline,
-          creditDeadline,
-        };
-      }
-
-      return { list, exchanges };
-    });
-}
 
-function buildCoinInfoFromDenom(
-  denom: DenominationRecord,
-  purseFee: AmountJson | undefined,
-  wireFee: AmountJson | undefined,
-  maxAge: number,
-  total: number,
-): CoinInfo {
-  return {
-    id: denom.denomPubHash,
-    denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
-    denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
-    denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
-    exchangePurse: purseFee,
-    exchangeWire: wireFee,
-    exchangeBaseUrl: denom.exchangeBaseUrl,
-    duration: AbsoluteTime.difference(
-      AbsoluteTime.now(),
-      AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
-    ),
-    totalAvailable: total,
-    value: DenominationRecord.getValue(denom),
-    maxAge,
-  };
-}
-
-export async function convertDepositAmount(
-  ws: InternalWalletState,
-  req: ConvertAmountRequest,
-): Promise<AmountResponse> {
-  const amount = Amounts.parseOrThrow(req.amount);
-  // const filter = getCoinsFilter(req);
-
-  const denoms = await getAvailableDenoms(
-    ws,
-    TransactionType.Deposit,
-    amount.currency,
-    {},
-  );
-  const result = convertDepositAmountForAvailableCoins(
-    denoms,
-    amount,
-    req.type,
-  );
 
-  return {
-    effectiveAmount: Amounts.stringify(result.effective),
-    rawAmount: Amounts.stringify(result.raw),
-  };
+export interface SelectedPeerCoin {
+  coinPub: string;
+  coinPriv: string;
+  contribution: AmountString;
+  denomPubHash: string;
+  denomSig: UnblindedSignature;
+  ageCommitmentProof: AgeCommitmentProof | undefined;
 }
 
-const LOG_REFRESH = false;
-const LOG_DEPOSIT = false;
-export function convertDepositAmountForAvailableCoins(
-  denoms: AvailableCoins,
-  amount: AmountJson,
-  mode: TransactionAmountMode,
-): AmountAndRefresh {
-  const zero = Amounts.zeroOfCurrency(amount.currency);
-  if (!denoms.list.length) {
-    // no coins in the database
-    return { effective: zero, raw: zero };
-  }
-  const depositDenoms = rankDenominationForDeposit(denoms.list, mode);
-
-  //FIXME: we are not taking into account
-  // * exchanges with multiple accounts
-  // * wallet with multiple exchanges
-  const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
-  const adjustedAmount = Amounts.add(amount, wireFee).amount;
-
-  const selected = selectGreedyCoins(depositDenoms, adjustedAmount);
-
-  const gap = Amounts.sub(amount, selected.totalValue).amount;
-
-  const result = getTotalEffectiveAndRawForDeposit(
-    selected.coins,
-    amount.currency,
-  );
-  result.raw = Amounts.sub(result.raw, wireFee).amount;
-
-  if (Amounts.isZero(gap)) {
-    // exact amount founds
-    return result;
-  }
-
-  if (LOG_DEPOSIT) {
-    const logInfo = selected.coins.map((c) => {
-      return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
-    });
-    console.log(
-      "deposit used:",
-      logInfo.join(", "),
-      "gap:",
-      Amounts.stringifyValue(gap),
-    );
-  }
+export interface PeerCoinSelectionDetails {
+  exchangeBaseUrl: string;
 
-  const refreshDenoms = rankDenominationForRefresh(denoms.list);
   /**
-   * FIXME: looking for refresh AFTER selecting greedy is not optimal
+   * Info of Coins that were selected.
    */
-  const refreshCoin = searchBestRefreshCoin(
-    depositDenoms,
-    refreshDenoms,
-    gap,
-    mode,
-  );
-
-  if (refreshCoin) {
-    const fee = Amounts.sub(result.effective, result.raw).amount;
-    const effective = Amounts.add(
-      result.effective,
-      refreshCoin.refreshEffective,
-    ).amount;
-    const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
-    //found with change
-    return {
-      effective,
-      raw,
-      refresh: refreshCoin,
-    };
-  }
+  coins: SelectedPeerCoin[];
 
-  // there is a gap, but no refresh coin was found
-  return result;
-}
-
-export async function getMaxDepositAmount(
-  ws: InternalWalletState,
-  req: GetAmountRequest,
-): Promise<AmountResponse> {
-  // const filter = getCoinsFilter(req);
-
-  const denoms = await getAvailableDenoms(
-    ws,
-    TransactionType.Deposit,
-    req.currency,
-    {},
-  );
-
-  const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
-  return {
-    effectiveAmount: Amounts.stringify(result.effective),
-    rawAmount: Amounts.stringify(result.raw),
-  };
+  /**
+   * How much of the deposit fees is the customer paying?
+   */
+  depositFees: AmountJson;
 }
 
-export function getMaxDepositAmountForAvailableCoins(
-  denoms: AvailableCoins,
-  currency: string,
-) {
-  const zero = Amounts.zeroOfCurrency(currency);
-  if (!denoms.list.length) {
-    // no coins in the database
-    return { effective: zero, raw: zero };
-  }
-
-  const result = getTotalEffectiveAndRawForDeposit(
-    denoms.list.map((info) => {
-      return { info, size: info.totalAvailable ?? 0 };
-    }),
-    currency,
-  );
-
-  const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
-  result.raw = Amounts.sub(result.raw, wireFee).amount;
-
-  return result;
-}
+/**
+ * Information about a selected coin for peer to peer payments.
+ */
+export interface PeerCoinInfo {
+  /**
+   * Public key of the coin.
+   */
+  coinPub: string;
 
-export async function convertPeerPushAmount(
-  ws: InternalWalletState,
-  req: ConvertAmountRequest,
-): Promise<AmountResponse> {
-  throw Error("to be implemented after 1.0");
-}
-export async function getMaxPeerPushAmount(
-  ws: InternalWalletState,
-  req: GetAmountRequest,
-): Promise<AmountResponse> {
-  throw Error("to be implemented after 1.0");
-}
-export async function convertWithdrawalAmount(
-  ws: InternalWalletState,
-  req: ConvertAmountRequest,
-): Promise<AmountResponse> {
-  const amount = Amounts.parseOrThrow(req.amount);
+  coinPriv: string;
 
-  const denoms = await getAvailableDenoms(
-    ws,
-    TransactionType.Withdrawal,
-    amount.currency,
-    {},
-  );
+  /**
+   * Deposit fee for the coin.
+   */
+  feeDeposit: AmountJson;
 
-  const result = convertWithdrawalAmountFromAvailableCoins(
-    denoms,
-    amount,
-    req.type,
-  );
+  value: AmountJson;
 
-  return {
-    effectiveAmount: Amounts.stringify(result.effective),
-    rawAmount: Amounts.stringify(result.raw),
-  };
-}
+  denomPubHash: string;
 
-export function convertWithdrawalAmountFromAvailableCoins(
-  denoms: AvailableCoins,
-  amount: AmountJson,
-  mode: TransactionAmountMode,
-) {
-  const zero = Amounts.zeroOfCurrency(amount.currency);
-  if (!denoms.list.length) {
-    // no coins in the database
-    return { effective: zero, raw: zero };
-  }
-  const withdrawDenoms = rankDenominationForWithdrawals(denoms.list, mode);
+  denomSig: UnblindedSignature;
 
-  const selected = selectGreedyCoins(withdrawDenoms, amount);
+  maxAge: number;
 
-  return getTotalEffectiveAndRawForWithdrawal(selected.coins, amount.currency);
+  ageCommitmentProof?: AgeCommitmentProof;
 }
 
-/** *****************************************************
- * HELPERS
- * *****************************************************
- */
-
-/**
- *
- * @param depositDenoms
- * @param refreshDenoms
- * @param amount
- * @param mode
- * @returns
- */
-function searchBestRefreshCoin(
-  depositDenoms: SelectableElement[],
-  refreshDenoms: Record<string, SelectableElement[]>,
-  amount: AmountJson,
-  mode: TransactionAmountMode,
-): RefreshChoice | undefined {
-  let choice: RefreshChoice | undefined = undefined;
-  let refreshIdx = 0;
-  refreshIteration: while (refreshIdx < depositDenoms.length) {
-    const d = depositDenoms[refreshIdx];
-
-    const denomContribution =
-      mode === TransactionAmountMode.Effective
-        ? d.value
-        : Amounts.sub(d.value, d.info.denomRefresh, 
d.info.denomDeposit).amount;
-
-    const changeAfterDeposit = Amounts.sub(denomContribution, amount).amount;
-    if (Amounts.isZero(changeAfterDeposit)) {
-      //this coin is not big enough to use for refresh
-      //since the list is sorted, we can break here
-      break refreshIteration;
-    }
-
-    const withdrawDenoms = refreshDenoms[d.info.exchangeBaseUrl];
-    const change = selectGreedyCoins(withdrawDenoms, changeAfterDeposit);
-
-    const zero = Amounts.zeroOfCurrency(amount.currency);
-    const withdrawChangeFee = change.coins.reduce((cur, prev) => {
-      return Amounts.add(
-        cur,
-        Amounts.mult(prev.info.denomWithdraw, prev.size).amount,
-      ).amount;
-    }, zero);
-
-    const withdrawChangeValue = change.coins.reduce((cur, prev) => {
-      return Amounts.add(cur, Amounts.mult(prev.info.value, prev.size).amount)
-        .amount;
-    }, zero);
-
-    const totalFee = Amounts.add(
-      d.info.denomDeposit,
-      d.info.denomRefresh,
-      withdrawChangeFee,
-    ).amount;
+export type SelectPeerCoinsResult =
+  | { type: "success"; result: PeerCoinSelectionDetails }
+  | {
+      type: "failure";
+      insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+    };
 
-    if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
-      //found cheaper change
-      choice = {
-        gap: amount,
-        totalFee: totalFee,
-        totalChangeValue: change.totalValue, //change after refresh
-        refreshEffective: Amounts.sub(d.info.value, 
withdrawChangeValue).amount, // what of the denom used is not recovered
-        selected: d.info,
-        coins: change.coins,
-      };
-    }
-    refreshIdx++;
-  }
-  if (choice) {
-    if (LOG_REFRESH) {
-      const logInfo = choice.coins.map((c) => {
-        return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
-      });
-      console.log(
-        "refresh used:",
-        Amounts.stringifyValue(choice.selected.value),
-        "change:",
-        logInfo.join(", "),
-        "fee:",
-        Amounts.stringifyValue(choice.totalFee),
-        "refreshEffective:",
-        Amounts.stringifyValue(choice.refreshEffective),
-        "totalChangeValue:",
-        Amounts.stringifyValue(choice.totalChangeValue),
-      );
-    }
-  }
-  return choice;
+export interface PeerCoinRepair {
+  exchangeBaseUrl: string;
+  coinPubs: CoinPublicKeyString[];
+  contribs: AmountJson[];
 }
 
-/**
- * Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForWithdrawals(
-  denoms: CoinInfo[],
-  mode: TransactionAmountMode,
-): SelectableElement[] {
-  const copyList = [...denoms];
-  /**
-   * Rank coins
-   */
-  copyList.sort((d1, d2) => {
-    // the best coin to use is
-    // 1.- the one that contrib more and pay less fee
-    // 2.- it takes more time before expires
-
-    //different exchanges may have different wireFee
-    //ranking should take the relative contribution in the exchange
-    //which is (value - denomFee / fixedFee)
-    const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
-    const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
-    const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
-    return (
-      contribCmp ||
-      Duration.cmp(d1.duration, d2.duration) ||
-      strcmp(d1.id, d2.id)
-    );
-  });
-
-  return copyList.map((info) => {
-    switch (mode) {
-      case TransactionAmountMode.Effective: {
-        //if the user instructed "effective" then we need to selected
-        //greedy total coin value
-        return {
-          info,
-          value: info.value,
-          total: Number.MAX_SAFE_INTEGER,
-        };
-      }
-      case TransactionAmountMode.Raw: {
-        //if the user instructed "raw" then we need to selected
-        //greedy total coin raw amount (without fee)
-        return {
-          info,
-          value: Amounts.add(info.value, info.denomWithdraw).amount,
-          total: Number.MAX_SAFE_INTEGER,
-        };
-      }
-    }
-  });
-}
+export interface PeerCoinSelectionRequest {
+  instructedAmount: AmountJson;
 
-/**
- * Returns a copy of the list sorted for the best denom to deposit first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForDeposit(
-  denoms: CoinInfo[],
-  mode: TransactionAmountMode,
-): SelectableElement[] {
-  const copyList = [...denoms];
   /**
-   * Rank coins
+   * Instruct the coin selection to repair this coin
+   * selection instead of selecting completely new coins.
    */
-  copyList.sort((d1, d2) => {
-    // the best coin to use is
-    // 1.- the one that contrib more and pay less fee
-    // 2.- it takes more time before expires
-
-    //different exchanges may have different wireFee
-    //ranking should take the relative contribution in the exchange
-    //which is (value - denomFee / fixedFee)
-    const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
-    const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
-    const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
-    return (
-      contribCmp ||
-      Duration.cmp(d1.duration, d2.duration) ||
-      strcmp(d1.id, d2.id)
-    );
-  });
-
-  return copyList.map((info) => {
-    switch (mode) {
-      case TransactionAmountMode.Effective: {
-        //if the user instructed "effective" then we need to selected
-        //greedy total coin value
-        return {
-          info,
-          value: info.value,
-          total: info.totalAvailable ?? 0,
-        };
-      }
-      case TransactionAmountMode.Raw: {
-        //if the user instructed "raw" then we need to selected
-        //greedy total coin raw amount (without fee)
-        return {
-          info,
-          value: Amounts.sub(info.value, info.denomDeposit).amount,
-          total: info.totalAvailable ?? 0,
-        };
-      }
-    }
-  });
+  repair?: PeerCoinRepair;
 }
 
-/**
- * Returns a copy of the list sorted for the best denom to withdraw first
- *
- * @param denoms
- * @returns
- */
-function rankDenominationForRefresh(
-  denoms: CoinInfo[],
-): Record<string, SelectableElement[]> {
-  const groupByExchange: Record<string, CoinInfo[]> = {};
-  for (const d of denoms) {
-    if (!groupByExchange[d.exchangeBaseUrl]) {
-      groupByExchange[d.exchangeBaseUrl] = [];
-    }
-    groupByExchange[d.exchangeBaseUrl].push(d);
-  }
-
-  const result: Record<string, SelectableElement[]> = {};
-  for (const d of denoms) {
-    result[d.exchangeBaseUrl] = rankDenominationForWithdrawals(
-      groupByExchange[d.exchangeBaseUrl],
-      TransactionAmountMode.Raw,
-    );
+export async function selectPeerCoins(
+  ws: InternalWalletState,
+  req: PeerCoinSelectionRequest,
+): Promise<SelectPeerCoinsResult> {
+  const instructedAmount = req.instructedAmount;
+  if (Amounts.isZero(instructedAmount)) {
+    // Other parts of the code assume that we have at least
+    // one coin to spend.
+    throw new Error("amount of zero not allowed");
   }
-  return result;
-}
-
-interface SelectableElement {
-  total: number;
-  value: AmountJson;
-  info: CoinInfo;
-}
-
-function selectGreedyCoins(
-  coins: SelectableElement[],
-  limit: AmountJson,
-): SelectedCoins {
-  const result: SelectedCoins = {
-    totalValue: Amounts.zeroOfCurrency(limit.currency),
-    coins: [],
-  };
-  if (!coins.length) return result;
-
-  let denomIdx = 0;
-  iterateDenoms: while (denomIdx < coins.length) {
-    const denom = coins[denomIdx];
-    // let total = denom.total;
-    const left = Amounts.sub(limit, result.totalValue).amount;
+  return await ws.db
+    .mktx((x) => [
+      x.exchanges,
+      x.contractTerms,
+      x.coins,
+      x.coinAvailability,
+      x.denominations,
+      x.refreshGroups,
+      x.peerPushPaymentInitiations,
+    ])
+    .runReadWrite(async (tx) => {
+      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;
+        }
+        // FIXME: Can't we do this faster by using coinAvailability?
+        const coins = (
+          await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
+        ).filter((x) => x.status === CoinStatus.Fresh);
+        const coinInfos: PeerCoinInfo[] = [];
+        for (const coin of coins) {
+          const denom = await ws.getDenomInfo(
+            ws,
+            tx,
+            coin.exchangeBaseUrl,
+            coin.denomPubHash,
+          );
+          if (!denom) {
+            throw Error("denom not found");
+          }
+          coinInfos.push({
+            coinPub: coin.coinPub,
+            feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
+            value: Amounts.parseOrThrow(denom.value),
+            denomPubHash: denom.denomPubHash,
+            coinPriv: coin.coinPriv,
+            denomSig: coin.denomSig,
+            maxAge: coin.maxAge,
+            ageCommitmentProof: coin.ageCommitmentProof,
+          });
+        }
+        if (coinInfos.length === 0) {
+          continue;
+        }
+        coinInfos.sort(
+          (o1, o2) =>
+            -Amounts.cmp(o1.value, o2.value) ||
+            strcmp(o1.denomPubHash, o2.denomPubHash),
+        );
+        let amountAcc = Amounts.zeroOfCurrency(currency);
+        let depositFeesAcc = Amounts.zeroOfCurrency(currency);
+        const resCoins: {
+          coinPub: string;
+          coinPriv: string;
+          contribution: AmountString;
+          denomPubHash: string;
+          denomSig: UnblindedSignature;
+          ageCommitmentProof: AgeCommitmentProof | undefined;
+        }[] = [];
+        let lastDepositFee = Amounts.zeroOfCurrency(currency);
+
+        if (req.repair) {
+          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 ws.getDenomInfo(
+              ws,
+              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);
+            lastDepositFee = depositFee;
+            amountAcc = Amounts.add(
+              amountAcc,
+              Amounts.sub(contrib, depositFee).amount,
+            ).amount;
+            depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
+          }
+        }
 
-    if (Amounts.isZero(denom.value)) {
-      // 0 contribution denoms should be the last
-      break iterateDenoms;
-    }
+        for (const coin of coinInfos) {
+          if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+            break;
+          }
+          const gap = Amounts.add(
+            coin.feeDeposit,
+            Amounts.sub(instructedAmount, amountAcc).amount,
+          ).amount;
+          const contrib = Amounts.min(gap, coin.value);
+          amountAcc = Amounts.add(
+            amountAcc,
+            Amounts.sub(contrib, coin.feeDeposit).amount,
+          ).amount;
+          depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
+          resCoins.push({
+            coinPriv: coin.coinPriv,
+            coinPub: coin.coinPub,
+            contribution: Amounts.stringify(contrib),
+            denomPubHash: coin.denomPubHash,
+            denomSig: coin.denomSig,
+            ageCommitmentProof: coin.ageCommitmentProof,
+          });
+          lastDepositFee = coin.feeDeposit;
+        }
+        if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
+          const res: PeerCoinSelectionDetails = {
+            exchangeBaseUrl: exch.baseUrl,
+            coins: resCoins,
+            depositFees: depositFeesAcc,
+          };
+          return { type: "success", result: res };
+        }
+        const diff = Amounts.sub(instructedAmount, amountAcc).amount;
+        exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, 
diff).amount;
 
-    //use Amounts.divmod instead of iterate
-    const div = Amounts.divmod(left, denom.value);
-    const size = Math.min(div.quotient, denom.total);
-    if (size > 0) {
-      const mul = Amounts.mult(denom.value, size).amount;
-      const progress = Amounts.add(result.totalValue, mul).amount;
+        continue;
+      }
 
-      result.totalValue = progress;
-      result.coins.push({ info: denom.info, size });
-      denom.total = denom.total - size;
-    }
+      // We were unable to select coins.
+      // Now we need to produce error details.
 
-    //go next denom
-    denomIdx++;
-  }
+      const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
+        currency,
+      });
 
-  return result;
-}
+      const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
 
-type AmountWithFee = { raw: AmountJson; effective: AmountJson };
-type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
+      let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
 
-export function getTotalEffectiveAndRawForDeposit(
-  list: { info: CoinInfo; size: number }[],
-  currency: string,
-): AmountWithFee {
-  const init = {
-    raw: Amounts.zeroOfCurrency(currency),
-    effective: Amounts.zeroOfCurrency(currency),
-  };
-  return list.reduce((prev, cur) => {
-    const ef = Amounts.mult(cur.info.value, cur.size).amount;
-    const rw = Amounts.mult(
-      Amounts.sub(cur.info.value, cur.info.denomDeposit).amount,
-      cur.size,
-    ).amount;
+      for (const exch of exchanges) {
+        if (exch.detailsPointer?.currency !== currency) {
+          continue;
+        }
+        const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, 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),
+        };
 
-    prev.effective = Amounts.add(prev.effective, ef).amount;
-    prev.raw = Amounts.add(prev.raw, rw).amount;
-    return prev;
-  }, init);
-}
+        maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
+      }
 
-function getTotalEffectiveAndRawForWithdrawal(
-  list: { info: CoinInfo; size: number }[],
-  currency: string,
-): AmountWithFee {
-  const init = {
-    raw: Amounts.zeroOfCurrency(currency),
-    effective: Amounts.zeroOfCurrency(currency),
-  };
-  return list.reduce((prev, cur) => {
-    const ef = Amounts.mult(cur.info.value, cur.size).amount;
-    const rw = Amounts.mult(
-      Amounts.add(cur.info.value, cur.info.denomWithdraw).amount,
-      cur.size,
-    ).amount;
+      const errDetails: PayPeerInsufficientBalanceDetails = {
+        amountRequested: Amounts.stringify(instructedAmount),
+        balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
+        balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
+        feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
+        perExchange,
+      };
 
-    prev.effective = Amounts.add(prev.effective, ef).amount;
-    prev.raw = Amounts.add(prev.raw, rw).amount;
-    return prev;
-  }, init);
+      return { type: "failure", insufficientBalanceDetails: errDetails };
+    });
 }
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts
similarity index 98%
copy from packages/taler-wallet-core/src/util/coinSelection.test.ts
copy to packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts
index fddd217ea..de8515d09 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.test.ts
@@ -22,12 +22,8 @@ import {
   TransactionAmountMode,
 } from "@gnu-taler/taler-util";
 import test, { ExecutionContext } from "ava";
-import {
-  CoinInfo,
-  convertDepositAmountForAvailableCoins,
-  convertWithdrawalAmountFromAvailableCoins,
-  getMaxDepositAmountForAvailableCoins,
-} from "./coinSelection.js";
+import { CoinInfo } from "./coinSelection.js";
+import { convertDepositAmountForAvailableCoins, 
getMaxDepositAmountForAvailableCoins, convertWithdrawalAmountFromAvailableCoins 
} from "./instructedAmountConversion.js";
 
 function makeCurrencyHelper(currency: string) {
   return (sx: TemplateStringsArray, ...vx: any[]) => {
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
similarity index 51%
copy from packages/taler-wallet-core/src/util/coinSelection.ts
copy to packages/taler-wallet-core/src/util/instructedAmountConversion.ts
index d3c6ffc67..bd02e7b22 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/instructedAmountConversion.ts
@@ -1,6 +1,6 @@
 /*
  This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2023 Taler Systems S.A.
 
  GNU Taler is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
@@ -14,787 +14,64 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-/**
- * Selection of coins for payments.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import { GlobalIDB } from "@gnu-taler/idb-bridge";
 import {
   AbsoluteTime,
-  AgeCommitmentProof,
   AgeRestriction,
   AmountJson,
   AmountResponse,
   Amounts,
-  CoinStatus,
   ConvertAmountRequest,
-  DenominationInfo,
-  DenominationPubKey,
-  DenomSelectionState,
   Duration,
-  ForcedCoinSel,
-  ForcedDenomSel,
   GetAmountRequest,
   GetPlanForOperationRequest,
-  j2s,
-  Logger,
-  parsePaytoUri,
-  PayCoinSelection,
-  PayMerchantInsufficientBalanceDetails,
-  strcmp,
   TransactionAmountMode,
   TransactionType,
+  parsePaytoUri,
+  strcmp,
 } from "@gnu-taler/taler-util";
+import { checkDbInvariant } from "./invariants.js";
 import {
-  AllowedAuditorInfo,
-  AllowedExchangeInfo,
   DenominationRecord,
-} from "../db.js";
-import {
-  CoinAvailabilityRecord,
+  InternalWalletState,
   getExchangeDetails,
-  isWithdrawableDenom,
 } from "../index.js";
-import { InternalWalletState } from "../internal-wallet-state.js";
-import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
-import { checkDbInvariant, checkLogicInvariant } from "./invariants.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;
-  feeDeposit: AmountJson;
-  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 CoinSelectionTally {
-  /**
-   * Amount that still needs to be paid.
-   * May increase during the computation when fees need to be covered.
-   */
-  amountPayRemaining: AmountJson;
-
-  /**
-   * Allowance given by the merchant towards wire fees
-   */
-  amountWireFeeLimitRemaining: AmountJson;
-
-  /**
-   * Allowance given by the merchant towards deposit fees
-   * (and wire fees after wire fee limit is exhausted)
-   */
-  amountDepositFeeLimitRemaining: AmountJson;
-
-  customerDepositFees: AmountJson;
-
-  customerWireFees: AmountJson;
-
-  wireFeeCoveredForExchange: Set<string>;
-
-  lastDepositFee: AmountJson;
-}
-
-/**
- * Account for the fees of spending a coin.
- */
-function tallyFees(
-  tally: Readonly<CoinSelectionTally>,
-  wireFeesPerExchange: Record<string, AmountJson>,
-  wireFeeAmortization: number,
-  exchangeBaseUrl: string,
-  feeDeposit: AmountJson,
-): CoinSelectionTally {
-  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,
-      wfForgiven,
-    ).amount;
-    // The remaining, amortized amount needs to be paid by the
-    // wallet or covered by the deposit fee allowance.
-    let wfRemaining = Amounts.divide(
-      Amounts.sub(wf, wfForgiven).amount,
-      wireFeeAmortization,
-    );
-
-    // This is the amount forgiven via the deposit fee allowance.
-    const wfDepositForgiven = Amounts.min(
-      amountDepositFeeLimitRemaining,
-      wfRemaining,
-    );
-    amountDepositFeeLimitRemaining = Amounts.sub(
-      amountDepositFeeLimitRemaining,
-      wfDepositForgiven,
-    ).amount;
-
-    wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
-    customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
-    amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
-    wireFeeCoveredForExchange.add(exchangeBaseUrl);
-  }
-
-  const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
-
-  amountDepositFeeLimitRemaining = Amounts.sub(
-    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,
-  };
-}
-
-export type SelectPayCoinsResult =
-  | {
-      type: "failure";
-      insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
-    }
-  | { type: "success"; coinSel: PayCoinSelection };
-
-/**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
- *
- * 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(
-  ws: InternalWalletState,
-  req: SelectPayCoinRequestNg,
-): Promise<SelectPayCoinsResult> {
-  const {
-    contractTermsAmount,
-    depositFeeLimit,
-    wireFeeLimit,
-    wireFeeAmortization,
-  } = req;
-
-  const [candidateDenoms, wireFeesPerExchange] = await selectCandidates(
-    ws,
-    req,
-  );
-
-  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),
-  };
-
-  const prevPayCoins = req.prevPayCoins ?? [];
-
-  // 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);
-  }
-
-  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) {
-    const details = await getMerchantPaymentBalanceDetails(ws, {
-      acceptedAuditors: req.auditors,
-      acceptedExchanges: req.exchanges,
-      acceptedWireMethods: [req.wireMethod],
-      currency: Amounts.currencyOf(req.contractTermsAmount),
-      minAge: req.requiredMinimumAge ?? 0,
-    });
-    let feeGapEstimate: AmountJson;
-    if (
-      Amounts.cmp(
-        details.balanceMerchantDepositable,
-        req.contractTermsAmount,
-      ) >= 0
-    ) {
-      // FIXME: We can probably give a better estimate.
-      feeGapEstimate = Amounts.add(
-        tally.amountPayRemaining,
-        tally.lastDepositFee,
-      ).amount;
-    } else {
-      feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount);
-    }
-    return {
-      type: "failure",
-      insufficientBalanceDetails: {
-        amountRequested: Amounts.stringify(req.contractTermsAmount),
-        balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
-        balanceAvailable: Amounts.stringify(details.balanceAvailable),
-        balanceMaterial: Amounts.stringify(details.balanceMaterial),
-        balanceMerchantAcceptable: Amounts.stringify(
-          details.balanceMerchantAcceptable,
-        ),
-        balanceMerchantDepositable: Amounts.stringify(
-          details.balanceMerchantDepositable,
-        ),
-        feeGapEstimate: Amounts.stringify(feeGapEstimate),
-      },
-    };
-  }
-
-  const finalSel = selectedDenom;
-
-  logger.trace(`coin selection request ${j2s(req)}`);
-  logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
-
-  await ws.db
-    .mktx((x) => [x.coins, x.denominations])
-    .runReadOnly(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.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})`,
-          );
-        }
-        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),
-    },
-  };
-}
-
-function makeAvailabilityKey(
-  exchangeBaseUrl: string,
-  denomPubHash: string,
-  maxAge: number,
-): string {
-  return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
-}
+import { CoinInfo } from "./coinSelection.js";
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
 
 /**
- * Selection result.
+ * If the operation going to be plan subtracts
+ * or adds amount in the wallet db
  */
-interface SelResult {
-  /**
-   * Map from an availability key
-   * to an array of contributions.
-   */
-  [avKey: string]: {
-    exchangeBaseUrl: string;
-    denomPubHash: string;
-    maxAge: number;
-    contributions: AmountJson[];
-  };
-}
-
-function selectGreedy(
-  req: SelectPayCoinRequestNg,
-  candidateDenoms: AvailableDenom[],
-  wireFeesPerExchange: Record<string, AmountJson>,
-  tally: CoinSelectionTally,
-): SelResult | undefined {
-  const { wireFeeAmortization } = req;
-  const selectedDenom: SelResult = {};
-  for (const denom of candidateDenoms) {
-    const contributions: AmountJson[] = [];
-
-    // Don't use this coin if depositing it is more expensive than
-    // the amount it would give the merchant.
-    if (Amounts.cmp(denom.feeDeposit, denom.value) > 0) {
-      tally.lastDepositFee = Amounts.parseOrThrow(denom.feeDeposit);
-      continue;
-    }
-
-    for (
-      let i = 0;
-      i < denom.numAvailable && Amounts.isNonZero(tally.amountPayRemaining);
-      i++
-    ) {
-      tally = tallyFees(
-        tally,
-        wireFeesPerExchange,
-        wireFeeAmortization,
-        denom.exchangeBaseUrl,
-        Amounts.parseOrThrow(denom.feeDeposit),
-      );
-
-      const coinSpend = Amounts.max(
-        Amounts.min(tally.amountPayRemaining, denom.value),
-        denom.feeDeposit,
-      );
-
-      tally.amountPayRemaining = Amounts.sub(
-        tally.amountPayRemaining,
-        coinSpend,
-      ).amount;
-
-      contributions.push(coinSpend);
-    }
-
-    if (contributions.length) {
-      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,
-        };
-      }
-      sd.contributions.push(...contributions);
-      selectedDenom[avKey] = sd;
-    }
-  }
-  return Amounts.isZero(tally.amountPayRemaining) ? selectedDenom : undefined;
-}
-
-function selectForced(
-  req: SelectPayCoinRequestNg,
-  candidateDenoms: AvailableDenom[],
-): SelResult | undefined {
-  const selectedDenom: SelResult = {};
-
-  const forcedSelection = req.forcedSelection;
-  checkLogicInvariant(!!forcedSelection);
-
-  for (const forcedCoin of forcedSelection.coins) {
-    let found = false;
-    for (const aci of candidateDenoms) {
-      if (aci.numAvailable <= 0) {
-        continue;
-      }
-      if (Amounts.cmp(aci.value, forcedCoin.value) === 0) {
-        aci.numAvailable--;
-        const avKey = makeAvailabilityKey(
-          aci.exchangeBaseUrl,
-          aci.denomPubHash,
-          aci.maxAge,
-        );
-        let sd = selectedDenom[avKey];
-        if (!sd) {
-          sd = {
-            contributions: [],
-            denomPubHash: aci.denomPubHash,
-            exchangeBaseUrl: aci.exchangeBaseUrl,
-            maxAge: aci.maxAge,
-          };
-        }
-        sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
-        selectedDenom[avKey] = sd;
-        found = true;
-        break;
-      }
-    }
-    if (!found) {
-      throw Error("can't find coin for forced coin selection");
-    }
-  }
-
-  return selectedDenom;
-}
-
-export interface SelectPayCoinRequestNg {
-  exchanges: AllowedExchangeInfo[];
-  auditors: AllowedAuditorInfo[];
-  wireMethod: string;
-  contractTermsAmount: AmountJson;
-  depositFeeLimit: AmountJson;
-  wireFeeLimit: AmountJson;
-  wireFeeAmortization: number;
-  prevPayCoins?: PreviousPayCoins;
-  requiredMinimumAge?: number;
-  forcedSelection?: ForcedCoinSel;
+export enum OperationType {
+  Credit = "credit",
+  Debit = "debit",
 }
 
-export type AvailableDenom = DenominationInfo & {
-  maxAge: number;
-  numAvailable: number;
-};
-
-async function selectCandidates(
-  ws: InternalWalletState,
-  req: SelectPayCoinRequestNg,
-): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
-  return await ws.db
-    .mktx((x) => [
-      x.exchanges,
-      x.exchangeDetails,
-      x.denominations,
-      x.coinAvailability,
-    ])
-    .runReadOnly(async (tx) => {
-      // FIXME: Use the existing helper (from balance.ts) to
-      // get acceptable exchanges.
-      const denoms: AvailableDenom[] = [];
-      const exchanges = await tx.exchanges.iter().toArray();
-      const wfPerExchange: Record<string, AmountJson> = {};
-      for (const exchange of exchanges) {
-        const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
-        // 1.- exchange has same currency
-        if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
-          continue;
-        }
-        let wireMethodFee: string | undefined;
-        // 2.- exchange supports wire method
-        for (const acc of exchangeDetails.wireInfo.accounts) {
-          const pp = parsePaytoUri(acc.payto_uri);
-          checkLogicInvariant(!!pp);
-          if (pp.targetType === req.wireMethod) {
-            // also check that wire method is supported now
-            const wireFeeStr = exchangeDetails.wireInfo.feesForType[
-              req.wireMethod
-            ]?.find((x) => {
-              return AbsoluteTime.isBetween(
-                AbsoluteTime.now(),
-                AbsoluteTime.fromProtocolTimestamp(x.startStamp),
-                AbsoluteTime.fromProtocolTimestamp(x.endStamp),
-              );
-            })?.wireFee;
-            if (wireFeeStr) {
-              wireMethodFee = wireFeeStr;
-            }
-            break;
-          }
-        }
-        if (!wireMethodFee) {
-          break;
-        }
-        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,
-          });
-        }
-      }
-      // 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];
-    });
+// FIXME: Name conflict ...
+interface ExchangeInfo {
+  wireFee: AmountJson | undefined;
+  purseFee: AmountJson | undefined;
+  creditDeadline: AbsoluteTime;
+  debitDeadline: AbsoluteTime;
 }
 
-/**
- * Get a list of denominations (with repetitions possible)
- * whose total value is as close as possible to the available
- * amount, but never larger.
- */
-export function selectWithdrawalDenominations(
-  amountAvailable: AmountJson,
-  denoms: DenominationRecord[],
-  denomselAllowLate: boolean = false,
-): DenomSelectionState {
-  let remaining = Amounts.copy(amountAvailable);
-
-  const selectedDenoms: {
-    count: number;
-    denomPubHash: string;
-  }[] = [];
-
-  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
-  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
-
-  denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
-  denoms.sort((d1, d2) =>
-    Amounts.cmp(
-      DenominationRecord.getValue(d2),
-      DenominationRecord.getValue(d1),
-    ),
-  );
-
-  for (const d of denoms) {
-    const cost = Amounts.add(
-      DenominationRecord.getValue(d),
-      d.fees.feeWithdraw,
-    ).amount;
-    const res = Amounts.divmod(remaining, cost);
-    const count = res.quotient;
-    remaining = Amounts.sub(remaining, Amounts.mult(cost, 
count).amount).amount;
-    if (count > 0) {
-      totalCoinValue = Amounts.add(
-        totalCoinValue,
-        Amounts.mult(DenominationRecord.getValue(d), count).amount,
-      ).amount;
-      totalWithdrawCost = Amounts.add(
-        totalWithdrawCost,
-        Amounts.mult(cost, count).amount,
-      ).amount;
-      selectedDenoms.push({
-        count,
-        denomPubHash: d.denomPubHash,
-      });
-    }
-
-    if (Amounts.isZero(remaining)) {
-      break;
-    }
-  }
-
-  if (logger.shouldLogTrace()) {
-    logger.trace(
-      `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`,
-    );
-    for (const sd of selectedDenoms) {
-      logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`);
-    }
-    logger.trace("(end of withdrawal denom list)");
+function getOperationType(txType: TransactionType): OperationType {
+  const operationType =
+    txType === TransactionType.Withdrawal
+      ? OperationType.Credit
+      : txType === TransactionType.Deposit
+      ? OperationType.Debit
+      : undefined;
+  if (!operationType) {
+    throw Error(`operation type ${txType} not yet supported`);
   }
-
-  return {
-    selectedDenoms,
-    totalCoinValue: Amounts.stringify(totalCoinValue),
-    totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
-  };
+  return operationType;
 }
 
-export function selectForcedWithdrawalDenominations(
-  amountAvailable: AmountJson,
-  denoms: DenominationRecord[],
-  forcedDenomSel: ForcedDenomSel,
-  denomselAllowLate: boolean,
-): DenomSelectionState {
-  const selectedDenoms: {
-    count: number;
-    denomPubHash: string;
-  }[] = [];
-
-  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
-  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
-
-  denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
-  denoms.sort((d1, d2) =>
-    Amounts.cmp(
-      DenominationRecord.getValue(d2),
-      DenominationRecord.getValue(d1),
-    ),
-  );
-
-  for (const fds of forcedDenomSel.denoms) {
-    const count = fds.count;
-    const denom = denoms.find((x) => {
-      return Amounts.cmp(DenominationRecord.getValue(x), fds.value) == 0;
-    });
-    if (!denom) {
-      throw Error(
-        `unable to find denom for forced selection (value ${fds.value})`,
-      );
-    }
-    const cost = Amounts.add(
-      DenominationRecord.getValue(denom),
-      denom.fees.feeWithdraw,
-    ).amount;
-    totalCoinValue = Amounts.add(
-      totalCoinValue,
-      Amounts.mult(DenominationRecord.getValue(denom), count).amount,
-    ).amount;
-    totalWithdrawCost = Amounts.add(
-      totalWithdrawCost,
-      Amounts.mult(cost, count).amount,
-    ).amount;
-    selectedDenoms.push({
-      count,
-      denomPubHash: denom.denomPubHash,
-    });
-  }
-
-  return {
-    selectedDenoms,
-    totalCoinValue: Amounts.stringify(totalCoinValue),
-    totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
-  };
+interface SelectedCoins {
+  totalValue: AmountJson;
+  coins: { info: CoinInfo; size: number }[];
+  refresh?: RefreshChoice;
 }
 
 function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
@@ -817,28 +94,6 @@ function getCoinsFilter(req: GetPlanForOperationRequest): 
CoinsFilter {
   }
 }
 
-/**
- * If the operation going to be plan subtracts
- * or adds amount in the wallet db
- */
-export enum OperationType {
-  Credit = "credit",
-  Debit = "debit",
-}
-
-function getOperationType(txType: TransactionType): OperationType {
-  const operationType =
-    txType === TransactionType.Withdrawal
-      ? OperationType.Credit
-      : txType === TransactionType.Deposit
-      ? OperationType.Debit
-      : undefined;
-  if (!operationType) {
-    throw Error(`operation type ${txType} not yet supported`);
-  }
-  return operationType;
-}
-
 interface RefreshChoice {
   /**
    * Amount that need to be covered
@@ -857,42 +112,18 @@ interface RefreshChoice {
   // totalChangeWithdrawalFee: AmountJson;
 }
 
-interface AvailableCoins {
-  list: CoinInfo[];
-  exchanges: Record<string, ExchangeInfo>;
-}
-interface SelectedCoins {
-  totalValue: AmountJson;
-  coins: { info: CoinInfo; size: number }[];
-  refresh?: RefreshChoice;
-}
-
-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;
-}
-interface ExchangeInfo {
-  wireFee: AmountJson | undefined;
-  purseFee: AmountJson | undefined;
-  creditDeadline: AbsoluteTime;
-  debitDeadline: AbsoluteTime;
-}
-
 interface CoinsFilter {
   shouldCalculatePurseFee?: boolean;
   exchanges?: string[];
   wireMethod?: string;
   ageRestricted?: number;
 }
+
+interface AvailableCoins {
+  list: CoinInfo[];
+  exchanges: Record<string, ExchangeInfo>;
+}
+
 /**
  * Get all the denoms that can be used for a operation that is limited
  * by the following restrictions.
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index b967571d0..bff4442b6 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -276,13 +276,6 @@ import {
 } from "./operations/withdraw.js";
 import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
 import { assertUnreachable } from "./util/assertUnreachable.js";
-import {
-  convertDepositAmount,
-  convertPeerPushAmount,
-  convertWithdrawalAmount,
-  getMaxDepositAmount,
-  getMaxPeerPushAmount,
-} from "./util/coinSelection.js";
 import {
   createTimeline,
   selectBestForOverlappingDenominations,
@@ -313,6 +306,13 @@ import {
   WalletCoreApiClient,
   WalletCoreResponseType,
 } from "./wallet-api-types.js";
+import {
+  convertDepositAmount,
+  getMaxDepositAmount,
+  convertPeerPushAmount,
+  getMaxPeerPushAmount,
+  convertWithdrawalAmount,
+} from "./util/instructedAmountConversion.js";
 
 const logger = new Logger("wallet.ts");
 

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