taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-core/src/coinSelection.ts | 20+++++++-------------
Mpackages/taler-wallet-core/src/dbtx-indexeddb.ts | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dbtx.ts | 35+++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/donau.ts | 13+++++++++----
Mpackages/taler-wallet-core/src/instructedAmountConversion.ts | 13++++---------
Mpackages/taler-wallet-core/src/withdraw.ts | 84++++++++++++++-----------------------------------------------------------------
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); } }