gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (5d0837913 -> 374d3498d)


From: gnunet
Subject: [taler-wallet-core] branch master updated (5d0837913 -> 374d3498d)
Date: Fri, 16 Sep 2022 16:36:29 +0200

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

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

    from 5d0837913 working on #7357
     new b7f7b9560 wallet-core: towards faster coin selection
     new 2747bc260 wallet-core: support forced coins in new coin selection algo
     new b91caf977 wallet-core: support age restrictions in new coin selection
     new 374d3498d -cleanup

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/idb-bridge/src/index.ts                   |  13 +-
 packages/taler-util/src/talerCrypto.ts             |   5 +
 packages/taler-util/src/walletTypes.ts             |   4 +
 .../src/crypto/cryptoImplementation.ts             |   1 +
 .../taler-wallet-core/src/crypto/cryptoTypes.ts    |   1 +
 packages/taler-wallet-core/src/db.ts               |  52 +-
 packages/taler-wallet-core/src/dbless.ts           |   4 +
 .../src/operations/backup/import.ts                |   3 +
 .../taler-wallet-core/src/operations/deposits.ts   |  60 +-
 packages/taler-wallet-core/src/operations/pay.ts   | 622 +++++++++++++--------
 .../src/operations/peer-to-peer.ts                 |  32 +-
 .../taler-wallet-core/src/operations/recoup.ts     |   8 +-
 .../taler-wallet-core/src/operations/refresh.ts    |  34 +-
 .../taler-wallet-core/src/operations/refund.ts     |   8 +-
 packages/taler-wallet-core/src/operations/tip.ts   |   4 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |  11 +-
 .../src/util/coinSelection.test.ts                 | 313 -----------
 .../taler-wallet-core/src/util/coinSelection.ts    | 251 +--------
 packages/taler-wallet-core/src/util/query.ts       |  15 +-
 packages/taler-wallet-core/src/wallet.ts           | 105 ++--
 20 files changed, 626 insertions(+), 920 deletions(-)
 delete mode 100644 packages/taler-wallet-core/src/util/coinSelection.test.ts

diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts
index c4dbb8281..825d41f5e 100644
--- a/packages/idb-bridge/src/index.ts
+++ b/packages/idb-bridge/src/index.ts
@@ -20,7 +20,7 @@ import {
   ObjectStoreRecord,
   MemoryBackendDump,
 } from "./MemoryBackend";
-import { Event } from "./idbtypes";
+import { Event, IDBKeyRange } from "./idbtypes";
 import {
   BridgeIDBCursor,
   BridgeIDBDatabase,
@@ -89,6 +89,17 @@ export type { AccessStats } from "./MemoryBackend";
   delete Object.prototype.__magic__;
 })();
 
+/**
+ * Global indexeddb objects, either from the native or bridge-idb
+ * implementation, depending on what is availabe in
+ * the global environment.
+ */
+export const GlobalIDB: {
+  KeyRange: typeof BridgeIDBKeyRange;
+} = {
+  KeyRange: (globalThis as any).IDBKeyRange ?? BridgeIDBKeyRange,
+};
+
 /**
  * Populate the global name space such that the given IndexedDB factory is made
  * available globally.
diff --git a/packages/taler-util/src/talerCrypto.ts 
b/packages/taler-util/src/talerCrypto.ts
index 8d2e41793..c9eeb0584 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -988,6 +988,11 @@ function invariant(cond: boolean): asserts cond {
 }
 
 export namespace AgeRestriction {
+  /**
+   * Smallest age value that the protocol considers "unrestricted".
+   */
+  export const AGE_UNRESTRICTED = 32;
+
   export function hashCommitment(ac: AgeCommitment): HashCodeString {
     const hc = new nacl.HashState();
     for (const pub of ac.publicKeys) {
diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index 7fcb752b1..6dcaac78d 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -694,6 +694,7 @@ const codecForDenominationInfo = (): 
Codec<DenominationInfo> =>
     .property("stampExpireWithdraw", codecForTimestamp)
     .property("stampExpireLegal", codecForTimestamp)
     .property("stampExpireDeposit", codecForTimestamp)
+    .property("exchangeBaseUrl", codecForString())
     .build("codecForDenominationInfo");
 
 export interface DenominationInfo {
@@ -747,6 +748,8 @@ export interface DenominationInfo {
    * Data after which coins of this denomination can't be deposited anymore.
    */
   stampExpireDeposit: TalerProtocolTimestamp;
+
+  exchangeBaseUrl: string;
 }
 
 export type Operation = "deposit" | "withdraw" | "refresh" | "refund";
@@ -1223,6 +1226,7 @@ export interface RefreshPlanchetInfo {
    */
   blindingKey: string;
 
+  maxAge: number;
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index 9eaf1d91e..8b2bcab32 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -1213,6 +1213,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
             coinPriv: encodeCrock(coinPriv),
             coinPub: encodeCrock(coinPub),
             coinEvHash: encodeCrock(coinEvHash),
+            maxAge: req.meltCoinMaxAge,
             ageCommitmentProof: newAc,
           };
           planchets.push(planchet);
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts 
b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index 6e0e01627..4c75aa91e 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -61,6 +61,7 @@ export interface DeriveRefreshSessionRequest {
   meltCoinPub: string;
   meltCoinPriv: string;
   meltCoinDenomPubHash: string;
+  meltCoinMaxAge: number;
   meltCoinAgeCommitmentProof?: AgeCommitmentProof;
   newCoinDenoms: RefreshNewDenomInfo[];
   feeRefresh: AmountJson;
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 2c4d55820..6466edf5a 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -319,11 +319,6 @@ export interface DenominationRecord {
    * that includes this denomination.
    */
   listIssueDate: TalerProtocolTimestamp;
-
-  /**
-   * Number of fresh coins of this denomination that are available.
-   */
-  freshCoinCount?: number;
 }
 
 export namespace DenominationRecord {
@@ -348,6 +343,7 @@ export namespace DenominationRecord {
       stampExpireWithdraw: d.stampExpireWithdraw,
       stampStart: d.stampStart,
       value: DenominationRecord.getValue(d),
+      exchangeBaseUrl: d.exchangeBaseUrl,
     };
   }
 }
@@ -545,6 +541,8 @@ export interface PlanchetRecord {
 
   coinEvHash: string;
 
+  maxAge: number;
+
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
@@ -673,6 +671,8 @@ export interface CoinRecord {
    */
   allocation?: CoinAllocation;
 
+  maxAge: number;
+
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
@@ -1769,7 +1769,45 @@ export interface OperationAttemptLongpollResult {
   type: OperationAttemptResultType.Longpoll;
 }
 
+/**
+ * Availability of coins of a given denomination (and age restriction!).
+ *
+ * We can't store this information with the denomination record, as one 
denomination
+ * can be withdrawn with multiple age restrictions.
+ */
+export interface CoinAvailabilityRecord {
+  currency: string;
+  amountVal: number;
+  amountFrac: number;
+  denomPubHash: string;
+  exchangeBaseUrl: string;
+
+  /**
+   * Age restriction on the coin, or 0 for no age restriction (or
+   * denomination without age restriction support).
+   */
+  maxAge: number;
+
+  /**
+   * Number of fresh coins of this denomination that are available.
+   */
+  freshCoinCount: number;
+}
+
 export const WalletStoresV1 = {
+  coinAvailability: describeStore(
+    "coinAvailability",
+    describeContents<CoinAvailabilityRecord>({
+      keyPath: ["exchangeBaseUrl", "denomPubHash", "maxAge"],
+    }),
+    {
+      byExchangeAgeAvailability: describeIndex("byExchangeAgeAvailability", [
+        "exchangeBaseUrl",
+        "maxAge",
+        "freshCoinCount",
+      ]),
+    },
+  ),
   coins: describeStore(
     "coins",
     describeContents<CoinRecord>({
@@ -1778,6 +1816,10 @@ export const WalletStoresV1 = {
     {
       byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
       byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
+      byExchangeDenomPubHashAndAgeAndStatus: describeIndex(
+        "byExchangeDenomPubHashAndAgeAndStatus",
+        ["exchangeBaseUrl", "denomPubHash", "maxAge", "status"],
+      ),
       byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"),
     },
   ),
diff --git a/packages/taler-wallet-core/src/dbless.ts 
b/packages/taler-wallet-core/src/dbless.ts
index 652ba8f53..ff7870435 100644
--- a/packages/taler-wallet-core/src/dbless.ts
+++ b/packages/taler-wallet-core/src/dbless.ts
@@ -49,6 +49,7 @@ import {
   BankWithdrawDetails,
   parseWithdrawUri,
   AmountJson,
+  AgeRestriction,
 } from "@gnu-taler/taler-util";
 import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
 import { DenominationRecord } from "./db.js";
@@ -86,6 +87,7 @@ export interface CoinInfo {
   denomPubHash: string;
   feeDeposit: string;
   feeRefresh: string;
+  maxAge: number;
 }
 
 /**
@@ -200,6 +202,7 @@ export async function withdrawCoin(args: {
     feeDeposit: Amounts.stringify(denom.fees.feeDeposit),
     feeRefresh: Amounts.stringify(denom.fees.feeRefresh),
     exchangeBaseUrl: args.exchangeBaseUrl,
+    maxAge: AgeRestriction.AGE_UNRESTRICTED,
   };
 }
 
@@ -298,6 +301,7 @@ export async function refreshCoin(req: {
         value: x.amountVal,
       },
     })),
+    meltCoinMaxAge: oldCoin.maxAge,
   });
 
   const meltReqBody: ExchangeMeltRequest = {
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 53dc50f3b..be09952cd 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -15,6 +15,7 @@
  */
 
 import {
+  AgeRestriction,
   AmountJson,
   Amounts,
   BackupCoinSourceType,
@@ -436,6 +437,8 @@ export async function importBackup(
                   ? CoinStatus.Fresh
                   : CoinStatus.Dormant,
                 coinSource,
+                // FIXME!
+                maxAge: AgeRestriction.AGE_UNRESTRICTED,
               });
             }
           }
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index e6f1591ee..22ec5f0a5 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -51,16 +51,14 @@ import {
   OperationStatus,
 } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
-import { selectPayCoins } from "../util/coinSelection.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 import { spendCoins } from "../wallet.js";
 import { getExchangeDetails } from "./exchanges.js";
 import {
-  CoinSelectionRequest,
   extractContractData,
   generateDepositPermissions,
-  getCandidatePayCoins,
   getTotalPaymentCost,
+  selectPayCoinsNew,
 } from "./pay.js";
 import { getTotalRefreshCost } from "./refresh.js";
 import { makeEventId } from "./transactions.js";
@@ -255,28 +253,17 @@ export async function getFeeForDeposit(
       }
     });
 
-  const csr: CoinSelectionRequest = {
-    allowedAuditors: [],
-    allowedExchanges: Object.values(exchangeInfos).map((v) => ({
+  const payCoinSel = await selectPayCoinsNew(ws, {
+    auditors: [],
+    exchanges: Object.values(exchangeInfos).map((v) => ({
       exchangeBaseUrl: v.url,
       exchangePub: v.master_pub,
     })),
-    amount: Amounts.parseOrThrow(req.amount),
-    maxDepositFee: Amounts.parseOrThrow(req.amount),
-    maxWireFee: Amounts.parseOrThrow(req.amount),
-    timestamp: TalerProtocolTimestamp.now(),
-    wireFeeAmortization: 1,
     wireMethod: p.targetType,
-  };
-
-  const candidates = await getCandidatePayCoins(ws, csr);
-
-  const payCoinSel = selectPayCoins({
-    candidates,
-    contractTermsAmount: csr.amount,
-    depositFeeLimit: csr.maxDepositFee,
-    wireFeeAmortization: csr.wireFeeAmortization,
-    wireFeeLimit: csr.maxWireFee,
+    contractTermsAmount: Amounts.parseOrThrow(req.amount),
+    depositFeeLimit: Amounts.parseOrThrow(req.amount),
+    wireFeeAmortization: 1,
+    wireFeeLimit: Amounts.parseOrThrow(req.amount),
     prevPayCoins: [],
   });
 
@@ -356,19 +343,10 @@ export async function prepareDepositGroup(
     "",
   );
 
-  const candidates = await getCandidatePayCoins(ws, {
-    allowedAuditors: contractData.allowedAuditors,
-    allowedExchanges: contractData.allowedExchanges,
-    amount: contractData.amount,
-    maxDepositFee: contractData.maxDepositFee,
-    maxWireFee: contractData.maxWireFee,
-    timestamp: contractData.timestamp,
-    wireFeeAmortization: contractData.wireFeeAmortization,
+  const payCoinSel = await selectPayCoinsNew(ws, {
+    auditors: contractData.allowedAuditors,
+    exchanges: contractData.allowedExchanges,
     wireMethod: contractData.wireMethod,
-  });
-
-  const payCoinSel = selectPayCoins({
-    candidates,
     contractTermsAmount: contractData.amount,
     depositFeeLimit: contractData.maxDepositFee,
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -459,19 +437,10 @@ export async function createDepositGroup(
     "",
   );
 
-  const candidates = await getCandidatePayCoins(ws, {
-    allowedAuditors: contractData.allowedAuditors,
-    allowedExchanges: contractData.allowedExchanges,
-    amount: contractData.amount,
-    maxDepositFee: contractData.maxDepositFee,
-    maxWireFee: contractData.maxWireFee,
-    timestamp: contractData.timestamp,
-    wireFeeAmortization: contractData.wireFeeAmortization,
+  const payCoinSel = await selectPayCoinsNew(ws, {
+    auditors: contractData.allowedAuditors,
+    exchanges: contractData.allowedExchanges,
     wireMethod: contractData.wireMethod,
-  });
-
-  const payCoinSel = selectPayCoins({
-    candidates,
     contractTermsAmount: contractData.amount,
     depositFeeLimit: contractData.maxDepositFee,
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -522,6 +491,7 @@ export async function createDepositGroup(
       x.recoupGroups,
       x.denominations,
       x.refreshGroups,
+      x.coinAvailability,
     ])
     .runReadWrite(async (tx) => {
       await spendCoins(ws, tx, {
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 5e3c3dd15..6b366f50d 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -24,8 +24,10 @@
 /**
  * Imports.
  */
+import { GlobalIDB } from "@gnu-taler/idb-bridge";
 import {
   AbsoluteTime,
+  AgeRestriction,
   AmountJson,
   Amounts,
   codecForContractTerms,
@@ -36,6 +38,7 @@ import {
   ConfirmPayResultType,
   ContractTerms,
   ContractTermsUtil,
+  DenominationInfo,
   Duration,
   encodeCrock,
   ForcedCoinSel,
@@ -44,12 +47,12 @@ import {
   j2s,
   Logger,
   NotificationType,
-  parsePaytoUri,
   parsePayUri,
   PayCoinSelection,
   PreparePayResult,
   PreparePayResultType,
   RefreshReason,
+  strcmp,
   TalerErrorCode,
   TalerErrorDetail,
   TalerProtocolTimestamp,
@@ -71,7 +74,6 @@ import {
   ProposalStatus,
   PurchaseRecord,
   WalletContractData,
-  WalletStoresV1,
 } from "../db.js";
 import {
   makeErrorDetail,
@@ -84,11 +86,9 @@ import {
 } from "../internal-wallet-state.js";
 import { assertUnreachable } from "../util/assertUnreachable.js";
 import {
-  AvailableCoinInfo,
-  CoinCandidateSelection,
+  CoinSelectionTally,
   PreviousPayCoins,
-  selectForcedPayCoins,
-  selectPayCoins,
+  tallyFees,
 } from "../util/coinSelection.js";
 import {
   getHttpResponseErrorDetails,
@@ -98,11 +98,11 @@ import {
   readUnexpectedResponseDetails,
   throwUnexpectedRequestError,
 } from "../util/http.js";
-import { GetReadWriteAccess } from "../util/query.js";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
 import { spendCoins } from "../wallet.js";
 import { getExchangeDetails } from "./exchanges.js";
-import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
+import { getTotalRefreshCost } from "./refresh.js";
 import { makeEventId } from "./transactions.js";
 
 /**
@@ -164,24 +164,6 @@ export async function getTotalPaymentCost(
     });
 }
 
-function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean 
{
-  if (denom.isRevoked) {
-    return false;
-  }
-  if (!denom.isOffered) {
-    return false;
-  }
-  if (coin.status !== CoinStatus.Fresh) {
-    return false;
-  }
-  if (
-    
AbsoluteTime.isExpired(AbsoluteTime.fromTimestamp(denom.stampExpireDeposit))
-  ) {
-    return false;
-  }
-  return true;
-}
-
 export interface CoinSelectionRequest {
   amount: AmountJson;
 
@@ -210,149 +192,6 @@ export interface CoinSelectionRequest {
   minimumAge?: number;
 }
 
-/**
- * Get candidate coins.  From these candidate coins,
- * the actual contributions will be computed later.
- *
- * The resulting candidate coin list is sorted deterministically.
- *
- * TODO: Exclude more coins:
- * - when we already have a coin with more remaining amount than
- *   the payment amount, coins with even higher amounts can be skipped.
- */
-export async function getCandidatePayCoins(
-  ws: InternalWalletState,
-  req: CoinSelectionRequest,
-): Promise<CoinCandidateSelection> {
-  const candidateCoins: AvailableCoinInfo[] = [];
-  const wireFeesPerExchange: Record<string, AmountJson> = {};
-
-  await ws.db
-    .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations, x.coins])
-    .runReadOnly(async (tx) => {
-      const exchanges = await tx.exchanges.iter().toArray();
-      for (const exchange of exchanges) {
-        let isOkay = false;
-        const exchangeDetails = await getExchangeDetails(tx, exchange.baseUrl);
-        if (!exchangeDetails) {
-          continue;
-        }
-        const exchangeFees = exchangeDetails.wireInfo;
-        if (!exchangeFees) {
-          continue;
-        }
-
-        const wireTypes = new Set<string>();
-        for (const acc of exchangeDetails.wireInfo.accounts) {
-          const p = parsePaytoUri(acc.payto_uri);
-          if (p) {
-            wireTypes.add(p.targetType);
-          }
-        }
-
-        if (!wireTypes.has(req.wireMethod)) {
-          // Exchange can't be used, because it doesn't support
-          // the wire type that the merchant requested.
-          continue;
-        }
-
-        // is the exchange explicitly allowed?
-        for (const allowedExchange of req.allowedExchanges) {
-          if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) 
{
-            isOkay = true;
-            break;
-          }
-        }
-
-        // is the exchange allowed because of one of its auditors?
-        if (!isOkay) {
-          for (const allowedAuditor of req.allowedAuditors) {
-            for (const auditor of exchangeDetails.auditors) {
-              if (auditor.auditor_pub === allowedAuditor.auditorPub) {
-                isOkay = true;
-                break;
-              }
-            }
-            if (isOkay) {
-              break;
-            }
-          }
-        }
-
-        if (!isOkay) {
-          continue;
-        }
-
-        const coins = await tx.coins.indexes.byBaseUrl
-          .iter(exchange.baseUrl)
-          .toArray();
-
-        if (!coins || coins.length === 0) {
-          continue;
-        }
-
-        // Denomination of the first coin, we assume that all other
-        // coins have the same currency
-        const firstDenom = await ws.getDenomInfo(
-          ws,
-          tx,
-          exchange.baseUrl,
-          coins[0].denomPubHash,
-        );
-        if (!firstDenom) {
-          throw Error("db inconsistent");
-        }
-        const currency = firstDenom.value.currency;
-        for (const coin of coins) {
-          const denom = await tx.denominations.get([
-            exchange.baseUrl,
-            coin.denomPubHash,
-          ]);
-          if (!denom) {
-            throw Error("db inconsistent");
-          }
-          if (denom.currency !== currency) {
-            logger.warn(
-              `same pubkey for different currencies at exchange 
${exchange.baseUrl}`,
-            );
-            continue;
-          }
-          if (!isSpendableCoin(coin, denom)) {
-            continue;
-          }
-          candidateCoins.push({
-            availableAmount: coin.currentAmount,
-            value: DenominationRecord.getValue(denom),
-            coinPub: coin.coinPub,
-            denomPub: denom.denomPub,
-            feeDeposit: denom.fees.feeDeposit,
-            exchangeBaseUrl: denom.exchangeBaseUrl,
-            ageCommitmentProof: coin.ageCommitmentProof,
-          });
-        }
-
-        let wireFee: AmountJson | undefined;
-        for (const fee of exchangeFees.feesForType[req.wireMethod] || []) {
-          if (
-            fee.startStamp <= req.timestamp &&
-            fee.endStamp >= req.timestamp
-          ) {
-            wireFee = fee.wireFee;
-            break;
-          }
-        }
-        if (wireFee) {
-          wireFeesPerExchange[exchange.baseUrl] = wireFee;
-        }
-      }
-    });
-
-  return {
-    candidateCoins,
-    wireFeesPerExchange,
-  };
-}
-
 /**
  * Record all information that is necessary to
  * pay for a proposal in the wallet's database.
@@ -407,6 +246,7 @@ async function recordConfirmPay(
       x.coins,
       x.refreshGroups,
       x.denominations,
+      x.coinAvailability,
     ])
     .runReadWrite(async (tx) => {
       const p = await tx.proposals.get(proposal.proposalId);
@@ -920,17 +760,6 @@ async function handleInsufficientFunds(
 
   const { contractData } = proposal.download;
 
-  const candidates = await getCandidatePayCoins(ws, {
-    allowedAuditors: contractData.allowedAuditors,
-    allowedExchanges: contractData.allowedExchanges,
-    amount: contractData.amount,
-    maxDepositFee: contractData.maxDepositFee,
-    maxWireFee: contractData.maxWireFee,
-    timestamp: contractData.timestamp,
-    wireFeeAmortization: contractData.wireFeeAmortization,
-    wireMethod: contractData.wireMethod,
-  });
-
   const prevPayCoins: PreviousPayCoins = [];
 
   await ws.db
@@ -962,8 +791,10 @@ async function handleInsufficientFunds(
       }
     });
 
-  const res = selectPayCoins({
-    candidates,
+  const res = await selectPayCoinsNew(ws, {
+    auditors: contractData.allowedAuditors,
+    exchanges: contractData.allowedExchanges,
+    wireMethod: contractData.wireMethod,
     contractTermsAmount: contractData.amount,
     depositFeeLimit: contractData.maxDepositFee,
     wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -980,7 +811,13 @@ async function handleInsufficientFunds(
   logger.trace("re-selected coins");
 
   await ws.db
-    .mktx((x) => [x.purchases, x.coins, x.denominations, x.refreshGroups])
+    .mktx((x) => [
+      x.purchases,
+      x.coins,
+      x.coinAvailability,
+      x.denominations,
+      x.refreshGroups,
+    ])
     .runReadWrite(async (tx) => {
       const p = await tx.purchases.get(proposalId);
       if (!p) {
@@ -1019,6 +856,362 @@ async function unblockBackup(
     });
 }
 
+export interface SelectPayCoinRequestNg {
+  exchanges: AllowedExchangeInfo[];
+  auditors: AllowedAuditorInfo[];
+  wireMethod: string;
+  contractTermsAmount: AmountJson;
+  depositFeeLimit: AmountJson;
+  wireFeeLimit: AmountJson;
+  wireFeeAmortization: number;
+  prevPayCoins?: PreviousPayCoins;
+  requiredMinimumAge?: number;
+  forcedSelection?: ForcedCoinSel;
+}
+
+export type AvailableDenom = DenominationInfo & {
+  maxAge: number;
+  numAvailable: number;
+};
+
+export 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) => {
+      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);
+        if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
+          continue;
+        }
+        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;
+        }
+        let ageLower = 0;
+        let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+        if (req.requiredMinimumAge) {
+          ageLower = req.requiredMinimumAge;
+        }
+        const myExchangeDenoms =
+          await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+            GlobalIDB.KeyRange.bound(
+              [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+              [
+                exchangeDetails.exchangeBaseUrl,
+                ageUpper,
+                Number.MAX_SAFE_INTEGER,
+              ],
+            ),
+          );
+        // FIXME: Check that the individual denomination is audited!
+        // FIXME: Should we exclude denominations that are
+        // not spendable anymore?
+        for (const denomAvail of myExchangeDenoms) {
+          const denom = await tx.denominations.get([
+            denomAvail.exchangeBaseUrl,
+            denomAvail.denomPubHash,
+          ]);
+          checkDbInvariant(!!denom);
+          denoms.push({
+            ...DenominationRecord.toDenomInfo(denom),
+            numAvailable: denomAvail.freshCoinCount ?? 0,
+            maxAge: denomAvail.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];
+    });
+}
+
+function makeAvailabilityKey(
+  exchangeBaseUrl: string,
+  denomPubHash: string,
+  maxAge: number,
+): string {
+  return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
+}
+
+/**
+ * Selection result.
+ */
+interface SelResult {
+  /**
+   * Map from an availability key
+   * to an array of contributions.
+   */
+  [avKey: string]: {
+    exchangeBaseUrl: string;
+    denomPubHash: string;
+    maxAge: number;
+    contributions: AmountJson[];
+  };
+}
+
+export function selectGreedy(
+  req: SelectPayCoinRequestNg,
+  candidateDenoms: AvailableDenom[],
+  wireFeesPerExchange: Record<string, AmountJson>,
+  tally: CoinSelectionTally,
+): SelResult | undefined {
+  const { wireFeeAmortization } = req;
+  const selectedDenom: SelResult = {};
+  for (const aci of candidateDenoms) {
+    const contributions: AmountJson[] = [];
+    for (let i = 0; i < aci.numAvailable; i++) {
+      // Don't use this coin if depositing it is more expensive than
+      // the amount it would give the merchant.
+      if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
+        continue;
+      }
+
+      if (Amounts.isZero(tally.amountPayRemaining)) {
+        // We have spent enough!
+        break;
+      }
+
+      tally = tallyFees(
+        tally,
+        wireFeesPerExchange,
+        wireFeeAmortization,
+        aci.exchangeBaseUrl,
+        aci.feeDeposit,
+      );
+
+      let coinSpend = Amounts.max(
+        Amounts.min(tally.amountPayRemaining, aci.value),
+        aci.feeDeposit,
+      );
+
+      tally.amountPayRemaining = Amounts.sub(
+        tally.amountPayRemaining,
+        coinSpend,
+      ).amount;
+      contributions.push(coinSpend);
+    }
+
+    if (contributions.length) {
+      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(...contributions);
+      selectedDenom[avKey] = sd;
+    }
+
+    if (Amounts.isZero(tally.amountPayRemaining)) {
+      return selectedDenom;
+    }
+  }
+  return undefined;
+}
+
+export 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;
+}
+
+/**
+ * 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<PayCoinSelection | undefined> {
+  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.getZero(currency),
+    customerWireFees: Amounts.getZero(currency),
+    wireFeeCoveredForExchange: new Set(),
+  };
+
+  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) {
+    return undefined;
+  }
+
+  const finalSel = selectedDenom;
+
+  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 {
+    paymentAmount: contractTermsAmount,
+    coinContributions,
+    coinPubs,
+    customerDepositFees: tally.customerDepositFees,
+    customerWireFees: tally.customerWireFees,
+  };
+}
+
 export async function checkPaymentByProposalId(
   ws: InternalWalletState,
   proposalId: string,
@@ -1069,24 +1262,16 @@ export async function checkPaymentByProposalId(
 
   if (!purchase) {
     // If not already paid, check if we could pay for it.
-    const candidates = await getCandidatePayCoins(ws, {
-      allowedAuditors: contractData.allowedAuditors,
-      allowedExchanges: contractData.allowedExchanges,
-      amount: contractData.amount,
-      maxDepositFee: contractData.maxDepositFee,
-      maxWireFee: contractData.maxWireFee,
-      timestamp: contractData.timestamp,
-      wireFeeAmortization: contractData.wireFeeAmortization,
-      wireMethod: contractData.wireMethod,
-    });
-    const res = selectPayCoins({
-      candidates,
+    const res = await selectPayCoinsNew(ws, {
+      auditors: contractData.allowedAuditors,
+      exchanges: contractData.allowedExchanges,
       contractTermsAmount: contractData.amount,
       depositFeeLimit: contractData.maxDepositFee,
       wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
       wireFeeLimit: contractData.maxWireFee,
       prevPayCoins: [],
       requiredMinimumAge: contractData.minimumAge,
+      wireMethod: contractData.wireMethod,
     });
 
     if (!res) {
@@ -1265,7 +1450,7 @@ export async function generateDepositPermissions(
     let wireInfoHash: string;
     wireInfoHash = contractData.wireInfoHash;
     logger.trace(
-      `signing deposit permission for coin with acp=${j2s(
+      `signing deposit permission for coin with ageRestriction=${j2s(
         coin.ageCommitmentProof,
       )}`,
     );
@@ -1313,7 +1498,7 @@ export async function runPayForConfirmPay(
       return {
         type: ConfirmPayResultType.Done,
         contractTerms: purchase.download.contractTermsRaw,
-        transactionId: makeEventId(TransactionType.Payment, proposalId)
+        transactionId: makeEventId(TransactionType.Payment, proposalId),
       };
     }
     case OperationAttemptResultType.Error:
@@ -1385,39 +1570,20 @@ export async function confirmPay(
 
   const contractData = d.contractData;
 
-  const candidates = await getCandidatePayCoins(ws, {
-    allowedAuditors: contractData.allowedAuditors,
-    allowedExchanges: contractData.allowedExchanges,
-    amount: contractData.amount,
-    maxDepositFee: contractData.maxDepositFee,
-    maxWireFee: contractData.maxWireFee,
-    timestamp: contractData.timestamp,
-    wireFeeAmortization: contractData.wireFeeAmortization,
-    wireMethod: contractData.wireMethod,
-  });
-
   let res: PayCoinSelection | undefined = undefined;
 
-  if (forcedCoinSel) {
-    res = selectForcedPayCoins(forcedCoinSel, {
-      candidates,
-      contractTermsAmount: contractData.amount,
-      depositFeeLimit: contractData.maxDepositFee,
-      wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
-      wireFeeLimit: contractData.maxWireFee,
-      requiredMinimumAge: contractData.minimumAge,
-    });
-  } else {
-    res = selectPayCoins({
-      candidates,
-      contractTermsAmount: contractData.amount,
-      depositFeeLimit: contractData.maxDepositFee,
-      wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
-      wireFeeLimit: contractData.maxWireFee,
-      prevPayCoins: [],
-      requiredMinimumAge: contractData.minimumAge,
-    });
-  }
+  res = await selectPayCoinsNew(ws, {
+    auditors: contractData.allowedAuditors,
+    exchanges: contractData.allowedExchanges,
+    wireMethod: contractData.wireMethod,
+    contractTermsAmount: contractData.amount,
+    depositFeeLimit: contractData.maxDepositFee,
+    wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
+    wireFeeLimit: contractData.maxWireFee,
+    prevPayCoins: [],
+    requiredMinimumAge: contractData.minimumAge,
+    forcedSelection: forcedCoinSel,
+  });
 
   logger.trace("coin selection result", res);
 
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts 
b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
index e71e8a709..48d422e0b 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -24,7 +24,8 @@ import {
   AcceptPeerPushPaymentRequest,
   AcceptPeerPushPaymentResponse,
   AgeCommitmentProof,
-  AmountJson, Amounts,
+  AmountJson,
+  Amounts,
   AmountString,
   buildCodecForObject,
   CheckPeerPullPaymentRequest,
@@ -34,7 +35,8 @@ import {
   Codec,
   codecForAmountString,
   codecForAny,
-  codecForExchangeGetContractResponse, constructPayPullUri,
+  codecForExchangeGetContractResponse,
+  constructPayPullUri,
   constructPayPushUri,
   ContractTermsUtil,
   decodeCrock,
@@ -58,14 +60,14 @@ import {
   TalerProtocolTimestamp,
   TransactionType,
   UnblindedSignature,
-  WalletAccountMergeFlags
+  WalletAccountMergeFlags,
 } from "@gnu-taler/taler-util";
 import {
   CoinStatus,
   MergeReserveInfo,
   ReserveRecordStatus,
   WalletStoresV1,
-  WithdrawalRecordType
+  WithdrawalRecordType,
 } from "../db.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
@@ -118,7 +120,8 @@ interface CoinInfo {
 
   denomSig: UnblindedSignature;
 
-  ageCommitmentProof: AgeCommitmentProof | undefined;
+  maxAge: number;
+  ageCommitmentProof?: AgeCommitmentProof;
 }
 
 export async function selectPeerCoins(
@@ -156,6 +159,7 @@ export async function selectPeerCoins(
         denomPubHash: denom.denomPubHash,
         coinPriv: coin.coinPriv,
         denomSig: coin.denomSig,
+        maxAge: coin.maxAge,
         ageCommitmentProof: coin.ageCommitmentProof,
       });
     }
@@ -245,6 +249,7 @@ export async function initiatePeerToPeerPush(
     .mktx((x) => [
       x.exchanges,
       x.coins,
+      x.coinAvailability,
       x.denominations,
       x.refreshGroups,
       x.peerPullPaymentInitiations,
@@ -336,7 +341,7 @@ export async function initiatePeerToPeerPush(
       exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
       contractPriv: econtractResp.contractPriv,
     }),
-    transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub)
+    transactionId: makeEventId(TransactionType.PeerPushDebit, pursePair.pub),
   };
 }
 
@@ -549,9 +554,9 @@ export async function acceptPeerPushPayment(
   return {
     transactionId: makeEventId(
       TransactionType.PeerPushCredit,
-      wg.withdrawalGroupId
-    )
-  }
+      wg.withdrawalGroupId,
+    ),
+  };
 }
 
 /**
@@ -583,6 +588,7 @@ export async function acceptPeerPullPayment(
       x.denominations,
       x.refreshGroups,
       x.peerPullPaymentIncoming,
+      x.coinAvailability,
     ])
     .runReadWrite(async (tx) => {
       const sel = await selectPeerCoins(ws, tx, instructedAmount);
@@ -641,8 +647,8 @@ export async function acceptPeerPullPayment(
     transactionId: makeEventId(
       TransactionType.PeerPullDebit,
       req.peerPullPaymentIncomingId,
-    )
-  }
+    ),
+  };
 }
 
 export async function checkPeerPullPayment(
@@ -836,7 +842,7 @@ export async function initiatePeerRequestForPay(
     }),
     transactionId: makeEventId(
       TransactionType.PeerPullCredit,
-      wg.withdrawalGroupId
-    )
+      wg.withdrawalGroupId,
+    ),
   };
 }
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts 
b/packages/taler-wallet-core/src/operations/recoup.ts
index 100bbc074..bd598511a 100644
--- a/packages/taler-wallet-core/src/operations/recoup.ts
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -392,7 +392,13 @@ export async function processRecoupGroupHandler(
   }
 
   await ws.db
-    .mktx((x) => [x.recoupGroups, x.denominations, x.refreshGroups, x.coins])
+    .mktx((x) => [
+      x.recoupGroups,
+      x.coinAvailability,
+      x.denominations,
+      x.refreshGroups,
+      x.coins,
+    ])
     .runReadWrite(async (tx) => {
       const rg2 = await tx.recoupGroups.get(recoupGroupId);
       if (!rg2) {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index 2d9ad2c05..e968ec020 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -77,7 +77,7 @@ import {
 import { checkDbInvariant } from "../util/invariants.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import { RetryInfo, runOperationHandlerForResult } from "../util/retries.js";
-import { makeCoinAvailable } from "../wallet.js";
+import { makeCoinAvailable, Wallet } from "../wallet.js";
 import { guardOperationException } from "./common.js";
 import { updateExchangeFromUrl } from "./exchanges.js";
 import {
@@ -368,6 +368,7 @@ async function refreshMelt(
     meltCoinPriv: oldCoin.coinPriv,
     meltCoinPub: oldCoin.coinPub,
     feeRefresh: oldDenom.feeRefresh,
+    meltCoinMaxAge: oldCoin.maxAge,
     meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
     newCoinDenoms,
     sessionSecretSeed: refreshSession.sessionSecretSeed,
@@ -614,6 +615,7 @@ async function refreshReveal(
     meltCoinPub: oldCoin.coinPub,
     feeRefresh: oldDenom.feeRefresh,
     newCoinDenoms,
+    meltCoinMaxAge: oldCoin.maxAge,
     meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
     sessionSecretSeed: refreshSession.sessionSecretSeed,
   });
@@ -676,6 +678,7 @@ async function refreshReveal(
           oldCoinPub: refreshGroup.oldCoinPubs[coinIndex],
         },
         coinEvHash: pc.coinEvHash,
+        maxAge: pc.maxAge,
         ageCommitmentProof: pc.ageCommitmentProof,
       };
 
@@ -684,7 +687,12 @@ async function refreshReveal(
   }
 
   await ws.db
-    .mktx((x) => [x.coins, x.denominations, x.refreshGroups])
+    .mktx((x) => [
+      x.coins,
+      x.denominations,
+      x.coinAvailability,
+      x.refreshGroups,
+    ])
     .runReadWrite(async (tx) => {
       const rg = await tx.refreshGroups.get(refreshGroupId);
       if (!rg) {
@@ -830,6 +838,7 @@ export async function createRefreshGroup(
     denominations: typeof WalletStoresV1.denominations;
     coins: typeof WalletStoresV1.coins;
     refreshGroups: typeof WalletStoresV1.refreshGroups;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
   }>,
   oldCoinPubs: CoinPublicKey[],
   reason: RefreshReason,
@@ -871,16 +880,15 @@ export async function createRefreshGroup(
     );
     if (coin.status !== CoinStatus.Dormant) {
       coin.status = CoinStatus.Dormant;
-      const denom = await tx.denominations.get([
+      const coinAv = await tx.coinAvailability.get([
         coin.exchangeBaseUrl,
         coin.denomPubHash,
+        coin.maxAge,
       ]);
-      checkDbInvariant(!!denom);
-      checkDbInvariant(
-        denom.freshCoinCount != null && denom.freshCoinCount > 0,
-      );
-      denom.freshCoinCount--;
-      await tx.denominations.put(denom);
+      checkDbInvariant(!!coinAv);
+      checkDbInvariant(coinAv.freshCoinCount > 0);
+      coinAv.freshCoinCount--;
+      await tx.coinAvailability.put(coinAv);
     }
     const refreshAmount = coin.currentAmount;
     inputPerCoin.push(refreshAmount);
@@ -967,7 +975,13 @@ export async function autoRefresh(
     durationFromSpec({ days: 1 }),
   );
   await ws.db
-    .mktx((x) => [x.coins, x.denominations, x.refreshGroups, x.exchanges])
+    .mktx((x) => [
+      x.coins,
+      x.denominations,
+      x.coinAvailability,
+      x.refreshGroups,
+      x.exchanges,
+    ])
     .runReadWrite(async (tx) => {
       const exchange = await tx.exchanges.get(exchangeBaseUrl);
       if (!exchange) {
diff --git a/packages/taler-wallet-core/src/operations/refund.ts 
b/packages/taler-wallet-core/src/operations/refund.ts
index 644b07ef1..bdcdac943 100644
--- a/packages/taler-wallet-core/src/operations/refund.ts
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -336,7 +336,13 @@ async function acceptRefunds(
   const now = TalerProtocolTimestamp.now();
 
   await ws.db
-    .mktx((x) => [x.purchases, x.coins, x.denominations, x.refreshGroups])
+    .mktx((x) => [
+      x.purchases,
+      x.coins,
+      x.coinAvailability,
+      x.denominations,
+      x.refreshGroups,
+    ])
     .runReadWrite(async (tx) => {
       const p = await tx.purchases.get(proposalId);
       if (!p) {
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index eef151cf2..9f96b7a7d 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -18,6 +18,7 @@
  * Imports.
  */
 import {
+  AgeRestriction,
   AcceptTipResponse,
   Amounts,
   BlindedDenominationSignature,
@@ -315,11 +316,12 @@ export async function processTip(
       exchangeBaseUrl: tipRecord.exchangeBaseUrl,
       status: CoinStatus.Fresh,
       coinEvHash: planchet.coinEvHash,
+      maxAge: AgeRestriction.AGE_UNRESTRICTED,
     });
   }
 
   await ws.db
-    .mktx((x) => [x.coins, x.denominations, x.tips])
+    .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
     .runReadWrite(async (tx) => {
       const tr = await tx.tips.get(walletTipId);
       if (!tr) {
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index f2152ccbc..cb0b55faf 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -22,6 +22,7 @@ import {
   AcceptManualWithdrawalResult,
   AcceptWithdrawalResponse,
   addPaytoQueryParams,
+  AgeRestriction,
   AmountJson,
   AmountLike,
   Amounts,
@@ -510,6 +511,7 @@ async function processPlanchetGenerate(
     withdrawalDone: false,
     withdrawSig: r.withdrawSig,
     withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+    maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
     ageCommitmentProof: r.ageCommitmentProof,
     lastError: undefined,
   };
@@ -823,6 +825,7 @@ async function processPlanchetVerifyAndStoreCoin(
       reservePub: planchet.reservePub,
       withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
     },
+    maxAge: planchet.maxAge,
     ageCommitmentProof: planchet.ageCommitmentProof,
   };
 
@@ -832,7 +835,13 @@ async function processPlanchetVerifyAndStoreCoin(
   // withdrawal succeeded.  If so, mark the withdrawal
   // group as finished.
   const firstSuccess = await ws.db
-    .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets])
+    .mktx((x) => [
+      x.coins,
+      x.denominations,
+      x.coinAvailability,
+      x.withdrawalGroups,
+      x.planchets,
+    ])
     .runReadWrite(async (tx) => {
       const p = await tx.planchets.get(planchetCoinPub);
       if (!p || p.withdrawalDone) {
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
deleted file mode 100644
index 55c007bbc..000000000
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import test from "ava";
-import { AmountJson, Amounts, DenomKeyType } from "@gnu-taler/taler-util";
-import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js";
-
-function a(x: string): AmountJson {
-  const amt = Amounts.parse(x);
-  if (!amt) {
-    throw Error("invalid amount");
-  }
-  return amt;
-}
-
-function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
-  return {
-    value: a(current),
-    availableAmount: a(current),
-    coinPub: "foobar",
-    denomPub: {
-      cipher: DenomKeyType.Rsa,
-      rsa_public_key: "foobar",
-      age_mask: 0,
-    },
-    feeDeposit: a(feeDeposit),
-    exchangeBaseUrl: "https://example.com/";,
-  };
-}
-
-function fakeAciWithAgeRestriction(current: string, feeDeposit: string): 
AvailableCoinInfo {
-  return {
-    value: a(current),
-    availableAmount: a(current),
-    coinPub: "foobar",
-    denomPub: {
-      cipher: DenomKeyType.Rsa,
-      rsa_public_key: "foobar",
-      age_mask: 2446657,
-    },
-    feeDeposit: a(feeDeposit),
-    exchangeBaseUrl: "https://example.com/";,
-  };
-}
-
-test("it should be able to pay if merchant takes the fees", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.1"),
-    fakeAci("EUR:1.0", "EUR:0.0"),
-  ];
-  acis.forEach((x, i) => (x.coinPub = String(i)));
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.1"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.deepEqual(res.coinPubs, ["1", "0"]);
-  t.pass();
-});
-
-test("it should take the last two coins if it pays less fees", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    fakeAci("EUR:1.0", "EUR:0.0"),
-    // Merchant covers the fee, this one shouldn't be used
-    fakeAci("EUR:1.0", "EUR:0.0"),
-  ];
-  acis.forEach((x, i) => (x.coinPub = String(i)));
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.5"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.deepEqual(res.coinPubs, ["1", "2"]);
-  t.pass();
-});
-
-test("it should take the last coins if the merchant doest not take all the 
fee", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    // this coin should be selected instead of previous one with fee
-    fakeAci("EUR:1.0", "EUR:0.0"),
-  ];
-  acis.forEach((x, i) => (x.coinPub = String(i)));
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.5"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.deepEqual(res.coinPubs, ["2", "0"]);
-  t.pass();
-});
-
-test("it should use 3 coins to cover fees and payment", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.5"), //contributed value 1 (fee by the merchant)
-    fakeAci("EUR:1.0", "EUR:0.5"), //contributed value .5
-    fakeAci("EUR:1.0", "EUR:0.5"), //contributed value .5
-  ];
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.5"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.true(res.coinPubs.length === 3);
-  t.pass();
-});
-
-test("it should return undefined if there is not enough coins", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    fakeAci("EUR:1.0", "EUR:0.5"),
-  ];
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:4.0"),
-    depositFeeLimit: a("EUR:0.2"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-
-  t.true(!res);
-  t.pass();
-});
-
-test("it should return undefined if there is not enough coins (taking into 
account fees)", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.5"),
-    fakeAci("EUR:1.0", "EUR:0.5"),
-  ];
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.2"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-  t.true(!res);
-  t.pass();
-});
-
-test("it should not count into customer fee if merchant can afford it", (t) => 
{
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.1"),
-    fakeAci("EUR:1.0", "EUR:0.1"),
-  ];
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:2.0"),
-    depositFeeLimit: a("EUR:0.2"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-  t.truthy(res);
-  t.true(Amounts.cmp(res!.customerDepositFees, "EUR:0.0") === 0);
-  t.true(
-    Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:2.0") === 0,
-  );
-  t.pass();
-});
-
-test("it should use the coins that spent less relative fee", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.2"),
-    fakeAci("EUR:0.1", "EUR:0.2"),
-    fakeAci("EUR:0.05", "EUR:0.05"),
-    fakeAci("EUR:0.05", "EUR:0.05"),
-  ];
-  acis.forEach((x, i) => (x.coinPub = String(i)));
-
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:1.1"),
-    depositFeeLimit: a("EUR:0.4"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.deepEqual(res.coinPubs, ["0", "2", "3"]);
-  t.pass();
-});
-
-test("coin selection 9", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAci("EUR:1.0", "EUR:0.2"),
-    fakeAci("EUR:0.2", "EUR:0.2"),
-  ];
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:1.2"),
-    depositFeeLimit: a("EUR:0.4"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-  });
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.true(res.coinContributions.length === 2);
-  t.true(
-    Amounts.cmp(Amounts.sum(res.coinContributions).amount, "EUR:1.2") === 0,
-  );
-  t.pass();
-});
-
-
-test("it should be able to use unrestricted coins for age restricted 
contract", (t) => {
-  const acis: AvailableCoinInfo[] = [
-    fakeAciWithAgeRestriction("EUR:1.0", "EUR:0.2"),
-    fakeAciWithAgeRestriction("EUR:0.2", "EUR:0.2"),
-  ];
-  const res = selectPayCoins({
-    candidates: {
-      candidateCoins: acis,
-      wireFeesPerExchange: {},
-    },
-    contractTermsAmount: a("EUR:1.2"),
-    depositFeeLimit: a("EUR:0.4"),
-    wireFeeLimit: a("EUR:0"),
-    wireFeeAmortization: 1,
-    requiredMinimumAge: 13
-  });
-  if (!res) {
-    t.fail();
-    return;
-  }
-  t.true(res.coinContributions.length === 2);
-  t.true(
-    Amounts.cmp(Amounts.sum(res.coinContributions).amount, "EUR:1.2") === 0,
-  );
-  t.pass();
-});
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 97e25abd3..12f87a920 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -25,15 +25,11 @@
  */
 import {
   AgeCommitmentProof,
-  AgeRestriction,
   AmountJson,
   Amounts,
   DenominationPubKey,
-  ForcedCoinSel,
   Logger,
-  PayCoinSelection,
 } from "@gnu-taler/taler-util";
-import { checkLogicInvariant } from "./invariants.js";
 
 const logger = new Logger("coinSelection.ts");
 
@@ -72,6 +68,7 @@ export interface AvailableCoinInfo {
 
   exchangeBaseUrl: string;
 
+  maxAge: number;
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
@@ -97,7 +94,7 @@ export interface SelectPayCoinRequest {
   requiredMinimumAge?: number;
 }
 
-interface CoinSelectionTally {
+export interface CoinSelectionTally {
   /**
    * Amount that still needs to be paid.
    * May increase during the computation when fees need to be covered.
@@ -125,7 +122,7 @@ interface CoinSelectionTally {
 /**
  * Account for the fees of spending a coin.
  */
-function tallyFees(
+export function tallyFees(
   tally: CoinSelectionTally,
   wireFeesPerExchange: Record<string, AmountJson>,
   wireFeeAmortization: number,
@@ -193,245 +190,3 @@ function tallyFees(
     wireFeeCoveredForExchange,
   };
 }
-
-/**
- * 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 function selectPayCoins(
-  req: SelectPayCoinRequest,
-): PayCoinSelection | undefined {
-  const {
-    candidates,
-    contractTermsAmount,
-    depositFeeLimit,
-    wireFeeLimit,
-    wireFeeAmortization,
-  } = req;
-
-  if (candidates.candidateCoins.length === 0) {
-    return undefined;
-  }
-  const coinPubs: string[] = [];
-  const coinContributions: AmountJson[] = [];
-  const currency = contractTermsAmount.currency;
-
-  let tally: CoinSelectionTally = {
-    amountPayRemaining: contractTermsAmount,
-    amountWireFeeLimitRemaining: wireFeeLimit,
-    amountDepositFeeLimitRemaining: depositFeeLimit,
-    customerDepositFees: Amounts.getZero(currency),
-    customerWireFees: Amounts.getZero(currency),
-    wireFeeCoveredForExchange: new Set(),
-  };
-
-  const prevPayCoins = req.prevPayCoins ?? [];
-
-  // Look at existing pay coin selection and tally up
-  for (const prev of prevPayCoins) {
-    tally = tallyFees(
-      tally,
-      candidates.wireFeesPerExchange,
-      wireFeeAmortization,
-      prev.exchangeBaseUrl,
-      prev.feeDeposit,
-    );
-    tally.amountPayRemaining = Amounts.sub(
-      tally.amountPayRemaining,
-      prev.contribution,
-    ).amount;
-
-    coinPubs.push(prev.coinPub);
-    coinContributions.push(prev.contribution);
-  }
-
-  const prevCoinPubs = new Set(prevPayCoins.map((x) => x.coinPub));
-
-  // Sort by available amount (descending),  deposit fee (ascending) and
-  // denomPub (ascending) if deposit fee is the same
-  // (to guarantee deterministic results)
-  const candidateCoins = [...candidates.candidateCoins].sort(
-    (o1, o2) =>
-      -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
-      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
-      DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
-  );
-
-  // 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.
-
-  for (const aci of candidateCoins) {
-    // Don't use this coin if depositing it is more expensive than
-    // the amount it would give the merchant.
-    if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) > 0) {
-      continue;
-    }
-
-    if (Amounts.isZero(tally.amountPayRemaining)) {
-      // We have spent enough!
-      break;
-    }
-
-    // The same coin can't contribute twice to the same payment,
-    // by a fundamental, intentional limitation of the protocol.
-    if (prevCoinPubs.has(aci.coinPub)) {
-      continue;
-    }
-
-    if (req.requiredMinimumAge != null) {
-      const index = AgeRestriction.getAgeGroupIndex(
-        aci.denomPub.age_mask,
-        req.requiredMinimumAge,
-      );
-      // if (!aci.ageCommitmentProof) {
-      //   // No age restriction, can't use for this payment
-      //   continue;
-      // }
-      if (
-        aci.ageCommitmentProof &&
-        aci.ageCommitmentProof.proof.privateKeys.length < index
-      ) {
-        // Available age proofs to low, can't use for this payment
-        continue;
-      }
-    }
-
-    tally = tallyFees(
-      tally,
-      candidates.wireFeesPerExchange,
-      wireFeeAmortization,
-      aci.exchangeBaseUrl,
-      aci.feeDeposit,
-    );
-
-    let coinSpend = Amounts.max(
-      Amounts.min(tally.amountPayRemaining, aci.availableAmount),
-      aci.feeDeposit,
-    );
-
-    tally.amountPayRemaining = Amounts.sub(
-      tally.amountPayRemaining,
-      coinSpend,
-    ).amount;
-    coinPubs.push(aci.coinPub);
-    coinContributions.push(coinSpend);
-  }
-
-  if (Amounts.isZero(tally.amountPayRemaining)) {
-    return {
-      paymentAmount: contractTermsAmount,
-      coinContributions,
-      coinPubs,
-      customerDepositFees: tally.customerDepositFees,
-      customerWireFees: tally.customerWireFees,
-    };
-  }
-  return undefined;
-}
-
-export function selectForcedPayCoins(
-  forcedCoinSel: ForcedCoinSel,
-  req: SelectPayCoinRequest,
-): PayCoinSelection | undefined {
-  const {
-    candidates,
-    contractTermsAmount,
-    depositFeeLimit,
-    wireFeeLimit,
-    wireFeeAmortization,
-  } = req;
-
-  if (candidates.candidateCoins.length === 0) {
-    return undefined;
-  }
-  const coinPubs: string[] = [];
-  const coinContributions: AmountJson[] = [];
-  const currency = contractTermsAmount.currency;
-
-  let tally: CoinSelectionTally = {
-    amountPayRemaining: contractTermsAmount,
-    amountWireFeeLimitRemaining: wireFeeLimit,
-    amountDepositFeeLimitRemaining: depositFeeLimit,
-    customerDepositFees: Amounts.getZero(currency),
-    customerWireFees: Amounts.getZero(currency),
-    wireFeeCoveredForExchange: new Set(),
-  };
-
-  // Not supported by forced coin selection
-  checkLogicInvariant(!req.prevPayCoins);
-
-  // Sort by available amount (descending),  deposit fee (ascending) and
-  // denomPub (ascending) if deposit fee is the same
-  // (to guarantee deterministic results)
-  const candidateCoins = [...candidates.candidateCoins].sort(
-    (o1, o2) =>
-      -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
-      Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
-      DenominationPubKey.cmp(o1.denomPub, o2.denomPub),
-  );
-
-  // 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.
-
-  // Set of spent coin indices from candidate coins
-  const spentSet: Set<number> = new Set();
-
-  for (const forcedCoin of forcedCoinSel.coins) {
-    let aci: AvailableCoinInfo | undefined = undefined;
-    for (let i = 0; i < candidateCoins.length; i++) {
-      if (spentSet.has(i)) {
-        continue;
-      }
-      if (
-        Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0
-      ) {
-        continue;
-      }
-      spentSet.add(i);
-      aci = candidateCoins[i];
-      break;
-    }
-
-    if (!aci) {
-      throw Error("can't find coin for forced coin selection");
-    }
-
-    tally = tallyFees(
-      tally,
-      candidates.wireFeesPerExchange,
-      wireFeeAmortization,
-      aci.exchangeBaseUrl,
-      aci.feeDeposit,
-    );
-
-    let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution);
-
-    tally.amountPayRemaining = Amounts.sub(
-      tally.amountPayRemaining,
-      coinSpend,
-    ).amount;
-    coinPubs.push(aci.coinPub);
-    coinContributions.push(coinSpend);
-  }
-
-  if (Amounts.isZero(tally.amountPayRemaining)) {
-    return {
-      paymentAmount: contractTermsAmount,
-      coinContributions,
-      coinPubs,
-      customerDepositFees: tally.customerDepositFees,
-      customerWireFees: tally.customerWireFees,
-    };
-  }
-  return undefined;
-}
diff --git a/packages/taler-wallet-core/src/util/query.ts 
b/packages/taler-wallet-core/src/util/query.ts
index 17b713659..8b8c30f35 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -33,6 +33,7 @@ import {
   IDBVersionChangeEvent,
   IDBCursor,
   IDBKeyPath,
+  IDBKeyRange,
 } from "@gnu-taler/idb-bridge";
 import { Logger } from "@gnu-taler/taler-util";
 import { performanceNow } from "./timer.js";
@@ -309,9 +310,12 @@ export function describeIndex(
 }
 
 interface IndexReadOnlyAccessor<RecordType> {
-  iter(query?: IDBValidKey): ResultStream<RecordType>;
+  iter(query?: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
   get(query: IDBValidKey): Promise<RecordType | undefined>;
-  getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
+  getAll(
+    query: IDBKeyRange | IDBValidKey,
+    count?: number,
+  ): Promise<RecordType[]>;
 }
 
 type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
@@ -319,9 +323,12 @@ type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
 };
 
 interface IndexReadWriteAccessor<RecordType> {
-  iter(query: IDBValidKey): ResultStream<RecordType>;
+  iter(query: IDBKeyRange | IDBValidKey): ResultStream<RecordType>;
   get(query: IDBValidKey): Promise<RecordType | undefined>;
-  getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
+  getAll(
+    query: IDBKeyRange | IDBValidKey,
+    count?: number,
+  ): Promise<RecordType[]>;
 }
 
 type GetIndexReadWriteAccess<RecordType, IndexMap> = {
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 49c7f77cf..812106c7a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -743,19 +743,9 @@ async function getExchangeDetailedInfo(
         return;
       }
 
-      const denominations: DenomInfo[] = denominationRecords.map((x) => ({
-        denomPub: x.denomPub,
-        denomPubHash: x.denomPubHash,
-        feeDeposit: x.fees.feeDeposit,
-        feeRefresh: x.fees.feeRefresh,
-        feeRefund: x.fees.feeRefund,
-        feeWithdraw: x.fees.feeWithdraw,
-        stampExpireDeposit: x.stampExpireDeposit,
-        stampExpireLegal: x.stampExpireLegal,
-        stampExpireWithdraw: x.stampExpireWithdraw,
-        stampStart: x.stampStart,
-        value: DenominationRecord.getValue(x),
-      }));
+      const denominations: DenomInfo[] = denominationRecords.map((x) =>
+        DenominationRecord.toDenomInfo(x),
+      );
 
       return {
         info: {
@@ -812,6 +802,7 @@ export async function makeCoinAvailable(
   ws: InternalWalletState,
   tx: GetReadWriteAccess<{
     coins: typeof WalletStoresV1.coins;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
     denominations: typeof WalletStoresV1.denominations;
   }>,
   coinRecord: CoinRecord,
@@ -821,12 +812,26 @@ export async function makeCoinAvailable(
     coinRecord.denomPubHash,
   ]);
   checkDbInvariant(!!denom);
-  if (!denom.freshCoinCount) {
-    denom.freshCoinCount = 0;
+  const ageRestriction = coinRecord.maxAge;
+  let car = await tx.coinAvailability.get([
+    coinRecord.exchangeBaseUrl,
+    coinRecord.denomPubHash,
+    ageRestriction,
+  ]);
+  if (!car) {
+    car = {
+      maxAge: ageRestriction,
+      amountFrac: denom.amountFrac,
+      amountVal: denom.amountVal,
+      currency: denom.currency,
+      denomPubHash: denom.denomPubHash,
+      exchangeBaseUrl: denom.exchangeBaseUrl,
+      freshCoinCount: 0,
+    };
   }
-  denom.freshCoinCount++;
+  car.freshCoinCount++;
   await tx.coins.put(coinRecord);
-  await tx.denominations.put(denom);
+  await tx.coinAvailability.put(car);
 }
 
 export interface CoinsSpendInfo {
@@ -843,6 +848,7 @@ export async function spendCoins(
   ws: InternalWalletState,
   tx: GetReadWriteAccess<{
     coins: typeof WalletStoresV1.coins;
+    coinAvailability: typeof WalletStoresV1.coinAvailability;
     refreshGroups: typeof WalletStoresV1.refreshGroups;
     denominations: typeof WalletStoresV1.denominations;
   }>,
@@ -853,11 +859,12 @@ export async function spendCoins(
     if (!coin) {
       throw Error("coin allocated for payment doesn't exist anymore");
     }
-    const denom = await tx.denominations.get([
+    const coinAvailability = await tx.coinAvailability.get([
       coin.exchangeBaseUrl,
       coin.denomPubHash,
+      coin.maxAge,
     ]);
-    checkDbInvariant(!!denom);
+    checkDbInvariant(!!coinAvailability);
     const contrib = csi.contributions[i];
     if (coin.status !== CoinStatus.Fresh) {
       const alloc = coin.allocation;
@@ -884,13 +891,15 @@ export async function spendCoins(
       throw Error("not enough remaining balance on coin for payment");
     }
     coin.currentAmount = remaining.amount;
-    checkDbInvariant(!!denom);
-    if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
-      throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+    checkDbInvariant(!!coinAvailability);
+    if (coinAvailability.freshCoinCount === 0) {
+      throw Error(
+        `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
+      );
     }
-    denom.freshCoinCount--;
+    coinAvailability.freshCoinCount--;
     await tx.coins.put(coin);
-    await tx.denominations.put(denom);
+    await tx.coinAvailability.put(coinAvailability);
   }
   const refreshCoinPubs = csi.coinPubs.map((x) => ({
     coinPub: x,
@@ -904,39 +913,45 @@ async function setCoinSuspended(
   suspended: boolean,
 ): Promise<void> {
   await ws.db
-    .mktx((x) => [x.coins, x.denominations])
+    .mktx((x) => [x.coins, x.coinAvailability])
     .runReadWrite(async (tx) => {
       const c = await tx.coins.get(coinPub);
       if (!c) {
         logger.warn(`coin ${coinPub} not found, won't suspend`);
         return;
       }
-      const denom = await tx.denominations.get([
+      const coinAvailability = await tx.coinAvailability.get([
         c.exchangeBaseUrl,
         c.denomPubHash,
+        c.maxAge,
       ]);
-      checkDbInvariant(!!denom);
+      checkDbInvariant(!!coinAvailability);
       if (suspended) {
         if (c.status !== CoinStatus.Fresh) {
           return;
         }
-        if (denom.freshCoinCount == null || denom.freshCoinCount === 0) {
-          throw Error(`invalid coin count ${denom.freshCoinCount} in DB`);
+        if (
+          coinAvailability.freshCoinCount == null ||
+          coinAvailability.freshCoinCount === 0
+        ) {
+          throw Error(
+            `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
+          );
         }
-        denom.freshCoinCount--;
+        coinAvailability.freshCoinCount--;
         c.status = CoinStatus.FreshSuspended;
       } else {
         if (c.status == CoinStatus.Dormant) {
           return;
         }
-        if (denom.freshCoinCount == null) {
-          denom.freshCoinCount = 0;
+        if (coinAvailability.freshCoinCount == null) {
+          coinAvailability.freshCoinCount = 0;
         }
-        denom.freshCoinCount++;
+        coinAvailability.freshCoinCount++;
         c.status = CoinStatus.Fresh;
       }
       await tx.coins.put(c);
-      await tx.denominations.put(denom);
+      await tx.coinAvailability.put(coinAvailability);
     });
 }
 
@@ -1205,7 +1220,12 @@ async function dispatchRequestInternal(
       const req = codecForForceRefreshRequest().decode(payload);
       const coinPubs = req.coinPubList.map((x) => ({ coinPub: x }));
       const refreshGroupId = await ws.db
-        .mktx((x) => [x.refreshGroups, x.denominations, x.coins])
+        .mktx((x) => [
+          x.refreshGroups,
+          x.coinAvailability,
+          x.denominations,
+          x.coins,
+        ])
         .runReadWrite(async (tx) => {
           return await createRefreshGroup(
             ws,
@@ -1591,20 +1611,7 @@ class InternalWalletStateImpl implements 
InternalWalletState {
     }
     const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
     if (d) {
-      const denomInfo = {
-        denomPub: d.denomPub,
-        denomPubHash: d.denomPubHash,
-        feeDeposit: d.fees.feeDeposit,
-        feeRefresh: d.fees.feeRefresh,
-        feeRefund: d.fees.feeRefund,
-        feeWithdraw: d.fees.feeWithdraw,
-        stampExpireDeposit: d.stampExpireDeposit,
-        stampExpireLegal: d.stampExpireLegal,
-        stampExpireWithdraw: d.stampExpireWithdraw,
-        stampStart: d.stampStart,
-        value: DenominationRecord.getValue(d),
-      };
-      return denomInfo;
+      return DenominationRecord.toDenomInfo(d);
     }
     return undefined;
   }

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