commit 985eb10f31bee73924a34304c0eb0aafcf6420d5
parent 71134277d046dad960d5d3223a45045bd0be49ac
Author: Florian Dold <dold@taler.net>
Date: Sun, 19 Jul 2026 01:40:59 +0200
db: move remaining cursor and key-range queries behind the DAL
Diffstat:
6 files changed, 137 insertions(+), 95 deletions(-)
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -412,17 +412,12 @@ async function assembleSelectPayCoinsSuccessResult(
for (const dph of Object.keys(finalSel)) {
const selInfo = finalSel[dph];
const numRequested = selInfo.contributions.length;
- const query = [
+ const coins = await tx.wtx.getFreshCoinsByDenomAndAge(
selInfo.exchangeBaseUrl,
selInfo.denomPubHash,
selInfo.maxAge,
- CoinStatus.Fresh,
- ];
- const coins =
- await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
- query,
- numRequested,
- );
+ numRequested,
+ );
if (coins.length != numRequested) {
throw Error(
`coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
@@ -1029,11 +1024,10 @@ async function selectPayCandidates(
}
const myExchangeCoins =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeDetails.exchangeBaseUrl, ageLower, 1],
- [exchangeDetails.exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
- ),
+ await tx.wtx.getCoinAvailabilityByExchangeAndAgeRange(
+ exchangeDetails.exchangeBaseUrl,
+ ageLower,
+ ageUpper,
);
if (logger.shouldLogTrace()) {
diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts
@@ -35,6 +35,7 @@ import {
ScopeType,
WalletNotification,
checkDbInvariant,
+ CoinStatus,
} from "@gnu-taler/taler-util";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
import {
@@ -48,6 +49,7 @@ import {
WalletDenomination,
WalletTransactionMeta,
DbPreciseTimestamp,
+ DbProtocolTimestamp,
WalletOperationRetry,
WalletContractTerms,
DenominationVerificationStatus,
@@ -402,6 +404,35 @@ export class IdbWalletTransaction implements WalletDbTransaction {
return await tx.denomLossEvents.iter().toArray();
}
+ async getFreshCoinsByDenomAndAge(
+ exchangeBaseUrl: string,
+ denomPubHash: string,
+ maxAge: number,
+ limit: number,
+ ): Promise<WalletCoin[]> {
+ const tx = this.tx;
+ return await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
+ [exchangeBaseUrl, denomPubHash, maxAge, CoinStatus.Fresh],
+ limit,
+ );
+ }
+
+ async getCoinAvailabilityByExchangeAndAgeRange(
+ exchangeBaseUrl: string,
+ ageLower: number,
+ ageUpper: number,
+ ): Promise<WalletCoinAvailability[]> {
+ const tx = this.tx;
+ // Lower bound of 1 on freshCoinCount: only denominations that actually
+ // have a fresh coin available.
+ return await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+ GlobalIDB.KeyRange.bound(
+ [exchangeBaseUrl, ageLower, 1],
+ [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+ ),
+ );
+ }
+
async getCoinsByExchange(exchangeBaseUrl: string): Promise<WalletCoin[]> {
const tx = this.tx;
return await tx.coins.indexes.byBaseUrl.getAll(exchangeBaseUrl);
@@ -1448,6 +1479,42 @@ export class IdbWalletTransaction implements WalletDbTransaction {
return await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
}
+ async findDenominationByFamilyFromExpiry(
+ denominationFamilySerial: number,
+ minStampExpireWithdraw: DbProtocolTimestamp,
+ match: (d: WalletDenomination) => boolean,
+ ): Promise<WalletDenomination | undefined> {
+ const tx = this.tx;
+ const cursor =
+ tx.denominations.indexes.byDenominationFamilySerialAndStampExpireWithdraw.iter();
+ // The cursor has to be positioned before it can be moved.
+ const first = await cursor.current();
+ if (!first.hasValue) {
+ return undefined;
+ }
+ if (
+ first.value.denominationFamilySerial < denominationFamilySerial ||
+ (first.value.denominationFamilySerial === denominationFamilySerial &&
+ first.value.stampExpireWithdraw < minStampExpireWithdraw)
+ ) {
+ cursor.continue([denominationFamilySerial, minStampExpireWithdraw]);
+ }
+ while (true) {
+ const cur = await cursor.current();
+ if (!cur.hasValue) {
+ return undefined;
+ }
+ if (cur.value.denominationFamilySerial != denominationFamilySerial) {
+ // Moved past this family.
+ return undefined;
+ }
+ if (match(cur.value)) {
+ return cur.value;
+ }
+ cursor.continue();
+ }
+ }
+
async getDenominationsByExchange(
exchangeBaseUrl: string,
): Promise<WalletDenomination[]> {
diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts
@@ -50,6 +50,7 @@ import {
WalletDenomination,
WalletTransactionMeta,
DbPreciseTimestamp,
+ DbProtocolTimestamp,
WalletOperationRetry,
WalletContractTerms,
DenominationVerificationStatus,
@@ -274,6 +275,26 @@ export interface WalletDbTransaction {
listAllDenomLossEvents(): Promise<WalletDenomLossEvent[]>;
+ /**
+ * Get up to `limit` fresh coins of a given denomination and age restriction.
+ */
+ getFreshCoinsByDenomAndAge(
+ exchangeBaseUrl: string,
+ denomPubHash: string,
+ maxAge: number,
+ limit: number,
+ ): Promise<WalletCoin[]>;
+
+ /**
+ * Get coin availability for an exchange across an age-restriction band,
+ * restricted to denominations that have at least one fresh coin.
+ */
+ getCoinAvailabilityByExchangeAndAgeRange(
+ exchangeBaseUrl: string,
+ ageLower: number,
+ ageUpper: number,
+ ): Promise<WalletCoinAvailability[]>;
+
getCoinsByExchange(exchangeBaseUrl: string): Promise<WalletCoin[]>;
countCoinsByExchange(exchangeBaseUrl: string): Promise<number>;
@@ -754,6 +775,20 @@ export interface WalletDbTransaction {
/**
* Get all denominations offered by an exchange.
*/
+ /**
+ * Find the first denomination of a family, scanning in withdraw-expiry order
+ * from the given timestamp, that satisfies the caller's predicate.
+ *
+ * The scan stops at the first match, so a family with many denominations
+ * normally costs a single record read. The predicate stays with the caller;
+ * only the ordered, early-terminating scan lives in the implementation.
+ */
+ findDenominationByFamilyFromExpiry(
+ denominationFamilySerial: number,
+ minStampExpireWithdraw: DbProtocolTimestamp,
+ match: (d: WalletDenomination) => boolean,
+ ): Promise<WalletDenomination | undefined>;
+
getDenominationsByExchange(
exchangeBaseUrl: string,
): Promise<WalletDenomination[]>;
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -130,7 +130,11 @@ async function submitDonationReceipts(
);
await wex.runLegacyWalletDbTx(async (tx) => {
- let donauSummary = await tx.wtx.getDonationSummary(group.donauBaseUrl, group.year, group.currency);
+ let donauSummary = await tx.wtx.getDonationSummary(
+ group.donauBaseUrl,
+ group.year,
+ group.currency,
+ );
if (!donauSummary) {
logger.warn(
`no donau summary (your database might be an old development version, upgrade not supported)`,
@@ -287,7 +291,7 @@ export async function handleSetDonau(
// Be idempotent, reuse old salt.
return;
}
- await tx.config.put({
+ await tx.wtx.upsertConfig({
key: ConfigRecordKey.DonauConfig,
value: {
donauBaseUrl: req.donauBaseUrl,
@@ -520,8 +524,9 @@ export async function generateDonauPlanchets(
if (!rec) {
return undefined;
}
- const existingPlanchets =
- await tx.wtx.countDonationPlanchetsByProposal(rec.proposalId);
+ const existingPlanchets = await tx.wtx.countDonationPlanchetsByProposal(
+ rec.proposalId,
+ );
if (existingPlanchets > 0) {
return;
}
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -230,15 +230,10 @@ async function getAvailableCoins(
const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
const myExchangeCoins =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeDetails.exchangeBaseUrl, ageLower, 1],
- [
- exchangeDetails.exchangeBaseUrl,
- ageUpper,
- Number.MAX_SAFE_INTEGER,
- ],
- ),
+ await tx.wtx.getCoinAvailabilityByExchangeAndAgeRange(
+ exchangeDetails.exchangeBaseUrl,
+ ageLower,
+ ageUpper,
);
//5.- save denoms with how many coins are available
// FIXME: Check that the individual denomination is audited!
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -1303,37 +1303,11 @@ export async function getWithdrawableDenomsTx(
const fpSerial = fam.denominationFamilySerial;
checkDbInvariant(typeof fpSerial === "number", "denominationFamilySerial");
// Now we need to find a representative denom for the family.
- let denom: WalletDenomination | undefined;
- const denomCursor =
- tx.denominations.indexes.byDenominationFamilySerialAndStampExpireWithdraw.iter();
- const dr0 = await denomCursor.current();
- if (!dr0.hasValue) {
- logger.warn(`no current denom for family ${fpSerial}`);
- continue;
- }
- if (
- dr0.value.denominationFamilySerial < fpSerial ||
- (dr0.value.denominationFamilySerial === fpSerial &&
- dr0.value.stampExpireWithdraw < dbNow)
- ) {
- denomCursor.continue([fpSerial, dbNow]);
- }
- while (1) {
- const dr = await denomCursor.current();
- if (!dr.hasValue) {
- break;
- }
- if (dr.value.denominationFamilySerial != fpSerial) {
- // Cursor went to next serial already, we need to stop.
- break;
- }
- if (isCandidateWithdrawableDenomRec(dr.value)) {
- denom = dr.value;
- break;
- }
-
- denomCursor.continue();
- }
+ const denom = await tx.wtx.findDenominationByFamilyFromExpiry(
+ fpSerial,
+ dbNow,
+ isCandidateWithdrawableDenomRec,
+ );
if (denom) {
relevantDenoms.push(denom);
} else {
@@ -2015,46 +1989,18 @@ export async function updateWithdrawalDenomsForExchange(
typeof fpSerial === "number",
"denominationFamilySerial",
);
- const denomCursor =
- tx.denominations.indexes.byDenominationFamilySerialAndStampExpireWithdraw.iter();
- // Need to wait for cursor to be positioned before we can move it.
- const dr0 = await denomCursor.current();
-
- if (!dr0.hasValue) {
- logger.warn(`no current denom for family ${fpSerial}`);
- continue;
- }
-
+ // Take the first withdrawable denomination of the family, and queue it
+ // for validation only if it has not been verified yet.
+ const dv = await tx.wtx.findDenominationByFamilyFromExpiry(
+ fpSerial,
+ dbNow,
+ isCandidateWithdrawableDenomRec,
+ );
if (
- dr0.value.denominationFamilySerial < fpSerial ||
- (dr0.value.denominationFamilySerial === fpSerial &&
- dr0.value.stampExpireWithdraw < dbNow)
+ dv &&
+ dv.verificationStatus === DenominationVerificationStatus.Unverified
) {
- denomCursor.continue([fpSerial, dbNow]);
- }
-
- while (1) {
- const dr = await denomCursor.current();
- if (!dr.hasValue) {
- break;
- }
-
- if (dr.value.denominationFamilySerial != fpSerial) {
- // Cursor went to next serial, we need to stop.
- break;
- }
-
- if (isCandidateWithdrawableDenomRec(dr.value)) {
- if (
- dr.value.verificationStatus ===
- DenominationVerificationStatus.Unverified
- ) {
- denominations.push(dr.value);
- }
- break;
- }
-
- denomCursor.continue();
+ denominations.push(dv);
}
}