taler-typescript-core

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

commit 111daccf6fe4ecf791fd7294d958d03abceea355
parent 886df1dc4a88d2b9eb372497e3751fbfe0532468
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Feb 2026 21:17:29 +0100

wallet-core: fix old denoms in fixup for denom families

Diffstat:
Mpackages/taler-wallet-core/src/db.ts | 111++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 10++++++++++
Mpackages/taler-wallet-core/src/withdraw.ts | 17+++++++----------
3 files changed, 88 insertions(+), 50 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -19,8 +19,10 @@ */ import { Event, + GlobalIDB, IDBDatabase, IDBFactory, + IDBKeyRange, IDBObjectStore, IDBRequest, IDBTransaction, @@ -3887,6 +3889,8 @@ export interface FixupDescription { /** * Manual migrations between minor versions of the DB schema. + * + * Fixups *must* be idempotent. */ export const walletDbFixups: FixupDescription[] = [ // Removing this would cause old transactions @@ -3912,48 +3916,75 @@ export const walletDbFixups: FixupDescription[] = [ // This migration creates denom families // for existing denomination records. { - fn: fixup20260129DenomFamilyMigration, - name: "fixup20260129DenomFamilyMigration", + fn: fixup20260203DenomFamilyMigration, + name: "fixup20260203DenomFamilyMigration", }, ]; -async function fixup20260129DenomFamilyMigration( +async function fixup20260203DenomFamilyMigration( tx: WalletDbAllStoresReadWriteTransaction, ): Promise<void> { - // FIXME: Batch the DB fetches, with some forEachAsyncBatched - await tx.denominations.iter().forEachAsync(async (r) => { - if (r.denominationFamilySerial != null) { - return; - } - const fp: DenomFamilyParams = { - exchangeBaseUrl: r.exchangeBaseUrl, - exchangeMasterPub: r.exchangeMasterPub, - feeDeposit: r.fees.feeDeposit, - feeRefresh: r.fees.feeRefresh, - feeRefund: r.fees.feeRefund, - feeWithdraw: r.fees.feeWithdraw, - value: r.value, - }; - const fph = hashDenomFamilyParams(fp); - const dfRec = - await tx.denominationFamilies.indexes.byFamilyParamsHash.get(fph); - let denominationFamilySerial; - if (dfRec) { - denominationFamilySerial = dfRec.denominationFamilySerial; - } else { - const insRes = await tx.denominationFamilies.put({ - familyParams: fp, - familyParamsHash: fph, - }); - denominationFamilySerial = insRes.key; + const batchSize = 500; + + let range: IDBKeyRange | undefined = undefined; + + while (1) { + const batch = await tx.denominations.getAll(range, batchSize); + logger.info(`fixing up batch of ${batch.length} denominations`); + + if (batch.length === 0) { + break; } - checkDbInvariant( - typeof denominationFamilySerial == "number", - "denominationFamilySerial", + + const last = batch[batch.length - 1]; + range = GlobalIDB.KeyRange.lowerBound( + [last.exchangeBaseUrl, last.denomPubHash], + true, ); - r.denominationFamilySerial = denominationFamilySerial; - await tx.denominations.put(r); - }); + + for (const r of batch) { + const fp: DenomFamilyParams = { + exchangeBaseUrl: r.exchangeBaseUrl, + exchangeMasterPub: r.exchangeMasterPub, + feeDeposit: r.fees.feeDeposit, + feeRefresh: r.fees.feeRefresh, + feeRefund: r.fees.feeRefund, + feeWithdraw: r.fees.feeWithdraw, + value: r.value, + }; + if (r.denominationFamilySerial != null) { + // Fast path: Check if family exists and is correct. + const oldFpRec = await tx.denominationFamilies.get( + r.denominationFamilySerial, + ); + if ( + oldFpRec && + canonicalJson(fp) == canonicalJson(oldFpRec.familyParams) + ) { + continue; + } + } + const fph = hashDenomFamilyParams(fp); + const dfRec = + await tx.denominationFamilies.indexes.byFamilyParamsHash.get(fph); + let denominationFamilySerial; + if (dfRec) { + denominationFamilySerial = dfRec.denominationFamilySerial; + } else { + const insRes = await tx.denominationFamilies.put({ + familyParams: fp, + familyParamsHash: fph, + }); + denominationFamilySerial = insRes.key; + } + checkDbInvariant( + typeof denominationFamilySerial == "number", + "denominationFamilySerial", + ); + r.denominationFamilySerial = denominationFamilySerial; + await tx.denominations.put(r); + } + } } async function fixup20260116BadRefreshCoinSelection( @@ -4051,12 +4082,12 @@ export async function applyFixups( ): Promise<number> { logger.trace("applying fixups"); let count = 0; - await db.runAllStoresReadWriteTx({}, async (tx) => { - for (const fixupInstruction of walletDbFixups) { + for (const fixupInstruction of walletDbFixups) { + await db.runAllStoresReadWriteTx({}, async (tx) => { logger.trace(`checking fixup ${fixupInstruction.name}`); const fixupRecord = await tx.fixups.get(fixupInstruction.name); if (fixupRecord) { - continue; + return; } logger.info(`applying DB fixup ${fixupInstruction.name}`); await fixupInstruction.fn(tx); @@ -4064,8 +4095,8 @@ export async function applyFixups( fixupName: fixupInstruction.name, }); count++; - } - }); + }); + } return count; } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -880,6 +880,11 @@ async function recoverStoredBackup( await importDb(wex.db.idbHandle(), bd); await wex.db.runAllStoresReadWriteTx({}, async (tx) => { await rematerializeTransactions(wex, tx); + // Clear fixups. Okay since they are idempotent. + const fixups = await tx.fixups.getAll(); + for (const f of fixups) { + await tx.fixups.delete(f.fixupName); + } }); logger.info(`import done`); } @@ -1817,6 +1822,11 @@ async function handleImportDb( await importDb(wex.db.idbHandle(), req.dump); await wex.db.runAllStoresReadWriteTx({}, async (tx) => { await rematerializeTransactions(wex, tx); + // Clear fixups. Okay since they are idempotent. + const fixups = await tx.fixups.getAll(); + for (const f of fixups) { + await tx.fixups.delete(f.fixupName); + } }); return {}; } diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -1437,11 +1437,9 @@ export async function getWithdrawableDenomsTx( continue; } if ( - !( - dr0.value.denominationFamilySerial > fpSerial || - (dr0.value.denominationFamilySerial === fpSerial && - dr0.value.stampExpireWithdraw >= dbNow) - ) + dr0.value.denominationFamilySerial < fpSerial || + (dr0.value.denominationFamilySerial === fpSerial && + dr0.value.stampExpireWithdraw < dbNow) ) { logger.trace(`continuing cursor past ${j2s([fpSerial, dbNow])}`); denomCursor.continue([fpSerial, dbNow]); @@ -1457,6 +1455,7 @@ export async function getWithdrawableDenomsTx( logger.trace(`cursor past target`); break; } + logger.trace(`considering ${j2s(dr.value)}`); if (isCandidateWithdrawableDenomRec(dr.value)) { denom = dr.value; break; @@ -2183,11 +2182,9 @@ export async function updateWithdrawalDenomsForExchange( continue; } if ( - !( - dr0.value.denominationFamilySerial > fpSerial || - (dr0.value.denominationFamilySerial === fpSerial && - dr0.value.stampExpireWithdraw >= dbNow) - ) + dr0.value.denominationFamilySerial < fpSerial || + (dr0.value.denominationFamilySerial === fpSerial && + dr0.value.stampExpireWithdraw < dbNow) ) { denomCursor.continue([fpSerial, dbNow]); }