gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 03/04: wallet-core: support age restrictions in new


From: gnunet
Subject: [taler-wallet-core] 03/04: wallet-core: support age restrictions in new coin selection
Date: Fri, 16 Sep 2022 16:36:32 +0200

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

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

commit b91caf977fad8da11e523ca3a39064dd86e04c64
Author: Florian Dold <florian@dold.me>
AuthorDate: Fri Sep 16 16:20:47 2022 +0200

    wallet-core: support age restrictions in new coin selection
---
 packages/idb-bridge/src/index.ts                   |  13 +-
 packages/taler-util/src/talerCrypto.ts             |   5 +
 packages/taler-util/src/walletTypes.ts             |   1 +
 .../src/crypto/cryptoImplementation.ts             |   1 +
 .../taler-wallet-core/src/crypto/cryptoTypes.ts    |   1 +
 packages/taler-wallet-core/src/db.ts               |  55 ++++-
 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   | 273 ++++++++-------------
 .../src/operations/peer-to-peer.ts                 |   6 +-
 .../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                 |  17 +-
 .../taler-wallet-core/src/util/coinSelection.ts    |   1 +
 packages/taler-wallet-core/src/util/query.ts       |  15 +-
 packages/taler-wallet-core/src/wallet.ts           |  74 ++++--
 20 files changed, 327 insertions(+), 267 deletions(-)

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 c3e5c6ed0..6dcaac78d 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -1226,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 760234941..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 {
@@ -546,6 +541,8 @@ export interface PlanchetRecord {
 
   coinEvHash: string;
 
+  maxAge: number;
+
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
@@ -674,6 +671,8 @@ export interface CoinRecord {
    */
   allocation?: CoinAllocation;
 
+  maxAge: number;
+
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
@@ -1770,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>({
@@ -1779,10 +1816,10 @@ export const WalletStoresV1 = {
     {
       byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"),
       byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"),
-      byDenomPubHashAndStatus: describeIndex("byDenomPubHashAndStatus", [
-        "denomPubHash",
-        "status",
-      ]),
+      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 9747f21a3..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 { selectPayCoinsLegacy } 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 = selectPayCoinsLegacy({
-    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 = selectPayCoinsLegacy({
-    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 = selectPayCoinsLegacy({
-    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 af6ff507f..ab59fff87 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -24,6 +24,7 @@
 /**
  * Imports.
  */
+import { BridgeIDBKeyRange, GlobalIDB } from "@gnu-taler/idb-bridge";
 import {
   AbsoluteTime,
   AgeRestriction,
@@ -102,7 +103,7 @@ import {
   readUnexpectedResponseDetails,
   throwUnexpectedRequestError,
 } from "../util/http.js";
-import { checkLogicInvariant } from "../util/invariants.js";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
 import { GetReadWriteAccess } from "../util/query.js";
 import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js";
 import { spendCoins } from "../wallet.js";
@@ -215,149 +216,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.
@@ -412,6 +270,7 @@ async function recordConfirmPay(
       x.coins,
       x.refreshGroups,
       x.denominations,
+      x.coinAvailability,
     ])
     .runReadWrite(async (tx) => {
       const p = await tx.proposals.get(proposal.proposalId);
@@ -976,7 +835,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) {
@@ -1029,6 +894,7 @@ export interface SelectPayCoinRequestNg {
 }
 
 export type AvailableDenom = DenominationInfo & {
+  maxAge: number;
   numAvailable: number;
 };
 
@@ -1037,7 +903,12 @@ async function selectCandidates(
   req: SelectPayCoinRequestNg,
 ): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
   return await ws.db
-    .mktx((x) => [x.exchanges, x.exchangeDetails, x.denominations])
+    .mktx((x) => [
+      x.exchanges,
+      x.exchangeDetails,
+      x.denominations,
+      x.coinAvailability,
+    ])
     .runReadOnly(async (tx) => {
       const denoms: AvailableDenom[] = [];
       const exchanges = await tx.exchanges.iter().toArray();
@@ -1065,17 +936,35 @@ async function selectCandidates(
         if (!accepted) {
           continue;
         }
-        // FIXME: Do this query more efficiently via indexing
-        const exchangeDenoms = await tx.denominations.indexes.byExchangeBaseUrl
-          .iter(exchangeDetails.exchangeBaseUrl)
-          .filter((x) => x.freshCoinCount != null && x.freshCoinCount > 0);
+        let ageLower = 0;
+        let ageUpper = Number.MAX_SAFE_INTEGER;
+        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 denom of exchangeDenoms) {
+        for (const denomAvail of myExchangeDenoms) {
+          const denom = await tx.denominations.get([
+            denomAvail.exchangeBaseUrl,
+            denomAvail.denomPubHash,
+          ]);
+          checkDbInvariant(!!denom);
           denoms.push({
             ...DenominationRecord.toDenomInfo(denom),
-            numAvailable: denom.freshCoinCount ?? 0,
+            numAvailable: denomAvail.freshCoinCount ?? 0,
+            maxAge: denomAvail.maxAge,
           });
         }
       }
@@ -1092,15 +981,28 @@ async function selectCandidates(
     });
 }
 
+function makeAvailabilityKey(
+  exchangeBaseUrl: string,
+  denomPubHash: string,
+  maxAge: number,
+): string {
+  return `${denomPubHash};${maxAge};${exchangeBaseUrl}`;
+}
+
 /**
  * Selection result.
  */
 interface SelResult {
   /**
-   * Map from denomination public key hashes
+   * Map from an availability key
    * to an array of contributions.
    */
-  [dph: string]: AmountJson[];
+  [avKey: string]: {
+    exchangeBaseUrl: string;
+    denomPubHash: string;
+    maxAge: number;
+    contributions: AmountJson[];
+  };
 }
 
 export function selectGreedy(
@@ -1146,7 +1048,22 @@ export function selectGreedy(
     }
 
     if (contributions.length) {
-      selectedDenom[aci.denomPubHash] = contributions;
+      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)) {
@@ -1173,9 +1090,22 @@ export function selectForced(
       }
       if (Amounts.cmp(aci.value, forcedCoin.value) === 0) {
         aci.numAvailable--;
-        const contributions = selectedDenom[aci.denomPubHash] ?? [];
-        contributions.push(Amounts.parseOrThrow(forcedCoin.value));
-        selectedDenom[aci.denomPubHash] = contributions;
+        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;
       }
@@ -1273,18 +1203,27 @@ export async function selectPayCoinsNew(
     .mktx((x) => [x.coins, x.denominations])
     .runReadOnly(async (tx) => {
       for (const dph of Object.keys(finalSel)) {
-        const contributions = finalSel[dph];
-        const coins = await tx.coins.indexes.byDenomPubHashAndStatus.getAll(
-          [dph, CoinStatus.Fresh],
-          contributions.length,
-        );
-        if (coins.length != contributions.length) {
+        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}/${contributions.length})`,
+            `coin selection failed (not available anymore, got only 
${coins.length}/${numRequested})`,
           );
         }
         coinPubs.push(...coins.map((x) => x.coinPub));
-        coinContributions.push(...contributions);
+        coinContributions.push(...selInfo.contributions);
       }
     });
 
@@ -1535,7 +1474,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,
       )}`,
     );
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..ffbc1fc97 100644
--- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts
+++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts
@@ -118,7 +118,8 @@ interface CoinInfo {
 
   denomSig: UnblindedSignature;
 
-  ageCommitmentProof: AgeCommitmentProof | undefined;
+  maxAge: number;
+  ageCommitmentProof?: AgeCommitmentProof;
 }
 
 export async function selectPeerCoins(
@@ -156,6 +157,7 @@ export async function selectPeerCoins(
         denomPubHash: denom.denomPubHash,
         coinPriv: coin.coinPriv,
         denomSig: coin.denomSig,
+        maxAge: coin.maxAge,
         ageCommitmentProof: coin.ageCommitmentProof,
       });
     }
@@ -245,6 +247,7 @@ export async function initiatePeerToPeerPush(
     .mktx((x) => [
       x.exchanges,
       x.coins,
+      x.coinAvailability,
       x.denominations,
       x.refreshGroups,
       x.peerPullPaymentInitiations,
@@ -583,6 +586,7 @@ export async function acceptPeerPullPayment(
       x.denominations,
       x.refreshGroups,
       x.peerPullPaymentIncoming,
+      x.coinAvailability,
     ])
     .runReadWrite(async (tx) => {
       const sel = await selectPeerCoins(ws, tx, instructedAmount);
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
index 3c6ad0d82..fe9672116 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -18,7 +18,12 @@
  * Imports.
  */
 import test from "ava";
-import { AmountJson, Amounts, DenomKeyType } from "@gnu-taler/taler-util";
+import {
+  AgeRestriction,
+  AmountJson,
+  Amounts,
+  DenomKeyType,
+} from "@gnu-taler/taler-util";
 import { AvailableCoinInfo, selectPayCoinsLegacy } from "./coinSelection.js";
 
 function a(x: string): AmountJson {
@@ -41,10 +46,14 @@ function fakeAci(current: string, feeDeposit: string): 
AvailableCoinInfo {
     },
     feeDeposit: a(feeDeposit),
     exchangeBaseUrl: "https://example.com/";,
+    maxAge: AgeRestriction.AGE_UNRESTRICTED,
   };
 }
 
-function fakeAciWithAgeRestriction(current: string, feeDeposit: string): 
AvailableCoinInfo {
+function fakeAciWithAgeRestriction(
+  current: string,
+  feeDeposit: string,
+): AvailableCoinInfo {
   return {
     value: a(current),
     availableAmount: a(current),
@@ -56,6 +65,7 @@ function fakeAciWithAgeRestriction(current: string, 
feeDeposit: string): Availab
     },
     feeDeposit: a(feeDeposit),
     exchangeBaseUrl: "https://example.com/";,
+    maxAge: AgeRestriction.AGE_UNRESTRICTED,
   };
 }
 
@@ -284,7 +294,6 @@ test("coin selection 9", (t) => {
   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"),
@@ -299,7 +308,7 @@ test("it should be able to use unrestricted coins for age 
restricted contract",
     depositFeeLimit: a("EUR:0.4"),
     wireFeeLimit: a("EUR:0"),
     wireFeeAmortization: 1,
-    requiredMinimumAge: 13
+    requiredMinimumAge: 13,
   });
   if (!res) {
     t.fail();
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 9622b3a76..d2f12baf5 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -72,6 +72,7 @@ export interface AvailableCoinInfo {
 
   exchangeBaseUrl: string;
 
+  maxAge: number;
   ageCommitmentProof?: AgeCommitmentProof;
 }
 
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 4751f7976..812106c7a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -802,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,
@@ -811,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 {
@@ -833,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;
   }>,
@@ -843,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;
@@ -874,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,
@@ -894,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);
     });
 }
 
@@ -1195,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,

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