diff options
| author | Florian Dold <florian@dold.me> | 2022-09-16 16:20:47 +0200 | 
|---|---|---|
| committer | Florian Dold <florian@dold.me> | 2022-09-16 16:32:21 +0200 | 
| commit | b91caf977fad8da11e523ca3a39064dd86e04c64 (patch) | |
| tree | 732e1543d2555094d7f9a9ca242309847c1a33a3 /packages/taler-wallet-core/src/operations | |
| parent | 2747bc260bc05418974570d04d7f999dfc988cda (diff) | |
wallet-core: support age restrictions in new coin selection
Diffstat (limited to 'packages/taler-wallet-core/src/operations')
9 files changed, 180 insertions, 227 deletions
| 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"; @@ -216,149 +217,6 @@ export interface CoinSelectionRequest {  }  /** - * 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) { | 
